Skip to content

Commit

Permalink
FINERACT-2081: Add all loan term validations to loan details
Browse files Browse the repository at this point in the history
  • Loading branch information
oleksii-novikov-onix authored and galovics committed Oct 12, 2024
1 parent fe16b61 commit 25866ed
Show file tree
Hide file tree
Showing 18 changed files with 383 additions and 161 deletions.
11 changes: 11 additions & 0 deletions fineract-avro-schemas/src/main/avro/loan/v1/LoanAccountDataV1.avsc
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,17 @@
}
]
},
{
"default": null,
"name": "loanTermVariations",
"type": [
"null",
{
"type": "array",
"items": "org.apache.fineract.avro.loan.v1.LoanTermVariationsDataV1"
}
]
},
{
"default": null,
"name": "clientActiveLoanOptions",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ private DataTableApiConstant() {
public static final String linkedAccountAssociateParamName = "linkedAccount";
public static final String multiDisburseDetailsAssociateParamName = "multiDisburseDetails";
public static final String futureScheduleAssociateParamName = "futureSchedule";
public static final String loanTermVariationsAssociateParamName = "loanTermVariations";
public static final String meetingAssociateParamName = "meeting";
public static final String emiAmountVariationsAssociateParamName = "emiAmountVariations";
public static final String collectionAssociateParamName = "collection";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.client.models.BatchResponse;
import org.apache.fineract.client.models.GetJournalEntriesTransactionIdResponse;
Expand Down Expand Up @@ -864,4 +865,18 @@ public static String wrongfixedLength(Integer actual, Integer expected) {
return String.format("Wrong value in LoanDeteils/fixedLength. %nActual value is: %s %nExpected Value is: %s", actualToStr,
expectedToStr);
}

public static String wrongValueInLineInLoanTermVariations(final int line, final List<List<String>> actual,
final List<String> expected) {
final String actualValues = actual.stream().map(List::toString).collect(Collectors.joining(System.lineSeparator()));

return String.format(
"%nWrong value in loan term variations tab line %d.%nActual values in line (with the same term variation applicable from) are:%n%s%nExpected values in line:%n%s",
line, actualValues, expected);
}

public static String wrongNumberOfLinesInLoanTermVariations(final int actual, final int expected) {
return String.format("Number of lines in loan term variations is not correct. Actual value is: %d - Expected value is: %d", actual,
expected);
}
}

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions fineract-e2e-tests-runner/src/test/resources/features/Loan.feature
Original file line number Diff line number Diff line change
Expand Up @@ -5728,3 +5728,32 @@ Feature: Loan
| 5 | 31 | 01 June 2024 | | 16.93 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 |
| 6 | 30 | 01 July 2024 | | 0.0 | 16.93 | 0.1 | 0.0 | 0.0 | 17.03 | 0.0 | 0.0 | 0.0 | 17.03 |
When Admin removes "LOAN_INTEREST_RECALCULATION" business step into LOAN_CLOSE_OF_BUSINESS workflow

Scenario: Loan Details Emi Amount Variations - AssociationsAll
Given Global configuration "is-interest-to-be-recovered-first-when-greater-than-emi" is enabled
Given Global configuration "enable-business-date" is enabled
When Admin sets the business date to "1 January 2023"
When Admin creates a client with random data
When Admin creates a fully customized loan with emi and 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 |
| LP1_INTEREST_DECLINING_BALANCE_SAR_RECALCULATION_SAME_AS_REPAYMENT_COMPOUNDING_NONE_MULTIDISB | 01 January 2023 | 10000 | 12 | DECLINING_BALANCE | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | PENALTIES_FEES_INTEREST_PRINCIPAL_ORDER |
And Admin successfully approves the loan on "1 January 2023" with "100" amount and expected disbursement date on "1 January 2023"
When Admin successfully disburse the loan on "1 January 2023" with "100" EUR transaction amount and "50" fixed emi amount
Then Loan emi amount variations has 1 variation, with the following data:
| Term Type Id | Term Type Code | Term Type Value | Applicable From | Decimal Value | Date Value | Is Specific To Installment | Is Processed |
| 1 | loanTermType.emiAmount | emiAmount | 01 January 2023 | 50.0 | | false | |

Scenario: Loan Details Loan Term Variations
Given Global configuration "is-interest-to-be-recovered-first-when-greater-than-emi" is enabled
Given Global configuration "enable-business-date" is enabled
When Admin sets the business date to "1 January 2023"
When Admin creates a client with random data
When Admin creates a fully customized loan with emi and 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 |
| LP1_INTEREST_DECLINING_BALANCE_SAR_RECALCULATION_SAME_AS_REPAYMENT_COMPOUNDING_NONE_MULTIDISB | 01 January 2023 | 10000 | 12 | DECLINING_BALANCE | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | PENALTIES_FEES_INTEREST_PRINCIPAL_ORDER |
And Admin successfully approves the loan on "1 January 2023" with "100" amount and expected disbursement date on "1 January 2023"
When Admin successfully disburse the loan on "1 January 2023" with "100" EUR transaction amount, "50" EUR fixed emi amount and adjust repayment date on "15 January 2023"
Then Loan term variations has 2 variation, with the following data:
| Term Type Id | Term Type Code | Term Type Value | Applicable From | Decimal Value | Date Value | Is Specific To Installment | Is Processed |
| 1 | loanTermType.emiAmount | emiAmount | 01 January 2023 | 50.0 | | false | |
| 4 | loanTermType.dueDate | dueDate | 01 February 2023 | 50.0 | 15 January 2023 | false | |
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType;
import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations;

@Getter
public class LoanTermVariationsData implements Comparable<LoanTermVariationsData> {
Expand All @@ -46,6 +47,16 @@ public LoanTermVariationsData(final Long id, final EnumOptionData termType, fina
this.isSpecificToInstallment = isSpecificToInstallment;
}

public LoanTermVariationsData(final Long id, final Integer termType, final LocalDate termVariationApplicableFrom,
final BigDecimal decimalValue, final LocalDate dateValue, final boolean isSpecificToInstallment) {
this.id = id;
this.termType = LoanEnumerations.loanVariationType(LoanTermVariationType.fromInt(termType));
this.termVariationApplicableFrom = termVariationApplicableFrom;
this.decimalValue = decimalValue;
this.dateValue = dateValue;
this.isSpecificToInstallment = isSpecificToInstallment;
}

public LoanTermVariationsData(final EnumOptionData termType, final LocalDate termVariationApplicableFrom, final BigDecimal decimalValue,
LocalDate dateValue, final boolean isSpecificToInstallment) {
this.id = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5366,16 +5366,12 @@ public boolean isForeclosure() {
return isForeClosure;
}

public Set<LoanTermVariations> getActiveLoanTermVariations() {
Set<LoanTermVariations> retData = new HashSet<>();
if (this.loanTermVariations != null && !this.loanTermVariations.isEmpty()) {
for (LoanTermVariations loanTermVariations : this.loanTermVariations) {
if (loanTermVariations.isActive()) {
retData.add(loanTermVariations);
}
}
public List<LoanTermVariations> getActiveLoanTermVariations() {
if (this.loanTermVariations == null) {
return new ArrayList<>();
}
return !retData.isEmpty() ? retData : null;

return this.loanTermVariations.stream().filter(LoanTermVariations::isActive).collect(Collectors.toList());
}

public boolean isTopup() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain;

import java.util.List;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
Expand All @@ -28,7 +29,24 @@
public interface LoanTermVariationsRepository
extends JpaRepository<LoanTermVariations, Long>, JpaSpecificationExecutor<LoanTermVariations> {

@Query("select lrr from LoanTermVariations lrr where lrr.loan.id = :loanId")
List<LoanTermVariations> findAllLoanTermVariationsByLoanId(@Param("loanId") Long loanId);
@Query("""
select new org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData(
ltv.id, ltv.termType, ltv.termApplicableFrom, ltv.decimalValue, ltv.dateValue, ltv.isSpecificToInstallment
)
from LoanTermVariations ltv
where ltv.loan.id = :loanId
order by ltv.termApplicableFrom
""")
List<LoanTermVariationsData> findLoanTermVariationsByLoanId(@Param("loanId") long loanId);

@Query("""
select new org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData(
ltv.id, ltv.termType, ltv.termApplicableFrom, ltv.decimalValue, ltv.dateValue, ltv.isSpecificToInstallment
)
from LoanTermVariations ltv
where ltv.loan.id = :loanId and ltv.termType = :termType
order by ltv.termApplicableFrom
""")
List<LoanTermVariationsData> findLoanTermVariationsByLoanIdAndTermType(@Param("loanId") long loanId, @Param("termType") int termType);

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@

import java.util.Collection;
import java.util.List;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.apache.avro.generic.GenericContainer;
import org.apache.commons.collections4.CollectionUtils;
Expand Down Expand Up @@ -86,9 +85,10 @@ public <T> ByteBufferSerializable toAvroDTO(BusinessEvent<T> rawEvent) {
List<LoanInstallmentDelinquencyBucketDataV1> installmentsDelinquencyData = installmentLevelDelinquencyEventProducer
.calculateInstallmentLevelDelinquencyData(event.get(), data.getCurrency());

Set<LoanTermVariations> activeLoanTermVariations = event.get().getActiveLoanTermVariations();
if (activeLoanTermVariations != null) {
data.setEmiAmountVariations(activeLoanTermVariations.stream().map(LoanTermVariations::toData).toList());
List<LoanTermVariations> activeLoanTermVariations = event.get().getActiveLoanTermVariations();

if (!activeLoanTermVariations.isEmpty()) {
data.setLoanTermVariations(activeLoanTermVariations.stream().map(LoanTermVariations::toData).toList());
}

LoanAccountDataV1 result = mapper.map(data);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ HAVING COUNT(BJI.JOB_INSTANCE_ID) <= :threshold

public Long getNotCompletedPartitionsCount(Long jobExecutionId, String partitionerStepName) {
return namedParameterJdbcTemplate.queryForObject("""
SELECT COUNT(bse.STEP_EXECUTION_ID)
SELECT COUNT(BSE.STEP_EXECUTION_ID)
FROM BATCH_STEP_EXECUTION BSE
WHERE
BSE.JOB_EXECUTION_ID = :jobExecutionId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleCalculationPlatformService;
import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleHistoryReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanTermVariationsRepository;
import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
Expand Down Expand Up @@ -287,6 +288,7 @@ public class LoansApiResource {
private final SqlValidator sqlValidator;
private final LoanSummaryBalancesRepository loanSummaryBalancesRepository;
private final ClientReadPlatformService clientReadPlatformService;
private final LoanTermVariationsRepository loanTermVariationsRepository;

/*
* This template API is used for loan approval, ideally this should be invoked on loan that are pending for
Expand Down Expand Up @@ -897,7 +899,8 @@ private String retrieveLoan(final Long loanId, final String loanExternalIdStr, b
Collection<NoteData> notes = null;
PortfolioAccountData linkedAccount = null;
Collection<DisbursementData> disbursementData = null;
Collection<LoanTermVariationsData> emiAmountVariations = null;
List<LoanTermVariationsData> emiAmountVariations = null;
List<LoanTermVariationsData> loanTermVariations = null;
Collection<LoanCollateralResponseData> loanCollateralManagements;
Collection<LoanCollateralManagementData> loanCollateralManagementData = new ArrayList<>();
CollectionData collectionData = this.delinquencyReadPlatformService.calculateLoanCollectionData(resolvedLoanId);
Expand All @@ -911,7 +914,8 @@ private String retrieveLoan(final Long loanId, final String loanExternalIdStr, b
DataTableApiConstant.transactionsAssociateParamName, DataTableApiConstant.chargesAssociateParamName,
DataTableApiConstant.guarantorsAssociateParamName, DataTableApiConstant.collateralAssociateParamName,
DataTableApiConstant.notesAssociateParamName, DataTableApiConstant.linkedAccountAssociateParamName,
DataTableApiConstant.multiDisburseDetailsAssociateParamName, DataTableApiConstant.collectionAssociateParamName));
DataTableApiConstant.multiDisburseDetailsAssociateParamName, DataTableApiConstant.collectionAssociateParamName,
DataTableApiConstant.loanTermVariationsAssociateParamName));
}

ApiParameterHelper.excludeAssociationsForResponseIfProvided(exclude, associationParameters);
Expand All @@ -938,10 +942,15 @@ private String retrieveLoan(final Long loanId, final String loanExternalIdStr, b
if (associationParameters.contains(DataTableApiConstant.emiAmountVariationsAssociateParamName)
|| associationParameters.contains(DataTableApiConstant.repaymentScheduleAssociateParamName)) {
mandatoryResponseParameters.add(DataTableApiConstant.emiAmountVariationsAssociateParamName);
emiAmountVariations = this.loanReadPlatformService.retrieveLoanTermVariations(resolvedLoanId,
emiAmountVariations = this.loanTermVariationsRepository.findLoanTermVariationsByLoanIdAndTermType(resolvedLoanId,
LoanTermVariationType.EMI_AMOUNT.getValue());
}

if (associationParameters.contains(DataTableApiConstant.loanTermVariationsAssociateParamName)) {
mandatoryResponseParameters.add(DataTableApiConstant.loanTermVariationsAssociateParamName);
loanTermVariations = this.loanTermVariationsRepository.findLoanTermVariationsByLoanId(resolvedLoanId);
}

if (associationParameters.contains(DataTableApiConstant.repaymentScheduleAssociateParamName)) {
mandatoryResponseParameters.add(DataTableApiConstant.repaymentScheduleAssociateParamName);
final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedData = new RepaymentScheduleRelatedLoanData(
Expand Down Expand Up @@ -1118,7 +1127,8 @@ private String retrieveLoan(final Long loanId, final String loanExternalIdStr, b
interestCalculationPeriodTypeOptions, fundOptions, chargeOptions, chargeTemplate, allowedLoanOfficers, loanPurposeOptions,
loanCollateralOptions, calendarOptions, notes, accountLinkingOptions, linkedAccount, disbursementData, emiAmountVariations,
overdueCharges, paidInAdvanceTemplate, interestRatesPeriods, clientActiveLoanOptions, rates, isRatesEnabled, collectionData,
LoanScheduleType.getValuesAsEnumOptionDataList(), LoanScheduleProcessingType.getValuesAsEnumOptionDataList());
LoanScheduleType.getValuesAsEnumOptionDataList(), LoanScheduleProcessingType.getValuesAsEnumOptionDataList(),
loanTermVariations);

final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters(),
mandatoryResponseParameters);
Expand Down
Loading

0 comments on commit 25866ed

Please sign in to comment.