FINERACT-2065: Schedule handling for fixed length configuration
This commit is contained in:
committed by
Adam Saghy
parent
6458094a5e
commit
b64b4b7105
+39
@@ -1832,4 +1832,43 @@ public final class LoanApplicationTerms {
|
|||||||
public Money getDownPaymentAmount() {
|
public Money getDownPaymentAmount() {
|
||||||
return downPaymentAmount;
|
return downPaymentAmount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Integer getFixedLength() {
|
||||||
|
return fixedLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate calculateMaxDateForFixedLength() {
|
||||||
|
final LocalDate startDate = getRepaymentStartDate();
|
||||||
|
LocalDate maxDateForFixedLength = null;
|
||||||
|
if (fixedLength == null) {
|
||||||
|
return maxDateForFixedLength;
|
||||||
|
}
|
||||||
|
switch (repaymentPeriodFrequencyType) {
|
||||||
|
case DAYS:
|
||||||
|
maxDateForFixedLength = startDate.plusDays(fixedLength);
|
||||||
|
break;
|
||||||
|
case WEEKS:
|
||||||
|
maxDateForFixedLength = startDate.plusWeeks(fixedLength);
|
||||||
|
break;
|
||||||
|
case MONTHS:
|
||||||
|
maxDateForFixedLength = startDate.plusMonths(fixedLength);
|
||||||
|
break;
|
||||||
|
case YEARS:
|
||||||
|
maxDateForFixedLength = startDate.plusYears(fixedLength);
|
||||||
|
break;
|
||||||
|
case INVALID:
|
||||||
|
break;
|
||||||
|
case WHOLE_TERM:
|
||||||
|
log.error("TODO Implement repaymentPeriodFrequencyType for WHOLE_TERM");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return maxDateForFixedLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getRepaymentStartDate() {
|
||||||
|
final RepaymentStartDateType repaymentStartDateType = getRepaymentStartDateType();
|
||||||
|
return RepaymentStartDateType.DISBURSEMENT_DATE.equals(repaymentStartDateType) ? getExpectedDisbursementDate()
|
||||||
|
: getSubmittedOnDate();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-6
@@ -34,7 +34,6 @@ import org.apache.fineract.portfolio.calendar.domain.CalendarHistory;
|
|||||||
import org.apache.fineract.portfolio.calendar.service.CalendarUtils;
|
import org.apache.fineract.portfolio.calendar.service.CalendarUtils;
|
||||||
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
|
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
|
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
|
||||||
import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@@ -46,16 +45,16 @@ public class DefaultScheduledDateGenerator implements ScheduledDateGenerator {
|
|||||||
|
|
||||||
final int numberOfRepayments = loanApplicationTerms.getNumberOfRepayments();
|
final int numberOfRepayments = loanApplicationTerms.getNumberOfRepayments();
|
||||||
|
|
||||||
RepaymentStartDateType repaymentStartDateType = loanApplicationTerms.getRepaymentStartDateType();
|
LocalDate lastRepaymentDate = loanApplicationTerms.getRepaymentStartDate();
|
||||||
|
|
||||||
LocalDate lastRepaymentDate = RepaymentStartDateType.DISBURSEMENT_DATE.equals(repaymentStartDateType)
|
|
||||||
? loanApplicationTerms.getExpectedDisbursementDate()
|
|
||||||
: loanApplicationTerms.getSubmittedOnDate();
|
|
||||||
boolean isFirstRepayment = true;
|
boolean isFirstRepayment = true;
|
||||||
for (int repaymentPeriod = 1; repaymentPeriod <= numberOfRepayments; repaymentPeriod++) {
|
for (int repaymentPeriod = 1; repaymentPeriod <= numberOfRepayments; repaymentPeriod++) {
|
||||||
lastRepaymentDate = generateNextRepaymentDate(lastRepaymentDate, loanApplicationTerms, isFirstRepayment);
|
lastRepaymentDate = generateNextRepaymentDate(lastRepaymentDate, loanApplicationTerms, isFirstRepayment);
|
||||||
isFirstRepayment = false;
|
isFirstRepayment = false;
|
||||||
}
|
}
|
||||||
|
if (loanApplicationTerms.getFixedLength() != null
|
||||||
|
&& loanApplicationTerms.calculateMaxDateForFixedLength().compareTo(lastRepaymentDate) != 0) {
|
||||||
|
lastRepaymentDate = loanApplicationTerms.calculateMaxDateForFixedLength();
|
||||||
|
}
|
||||||
lastRepaymentDate = adjustRepaymentDate(lastRepaymentDate, loanApplicationTerms, holidayDetailDTO).getChangedScheduleDate();
|
lastRepaymentDate = adjustRepaymentDate(lastRepaymentDate, loanApplicationTerms, holidayDetailDTO).getChangedScheduleDate();
|
||||||
return lastRepaymentDate;
|
return lastRepaymentDate;
|
||||||
}
|
}
|
||||||
@@ -104,6 +103,12 @@ public class DefaultScheduledDateGenerator implements ScheduledDateGenerator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final LocalDate maxDateForFixedLength = loanApplicationTerms.calculateMaxDateForFixedLength();
|
||||||
|
// Fixed Length validation
|
||||||
|
if (maxDateForFixedLength != null && DateUtils.isAfter(dueRepaymentPeriodDate, maxDateForFixedLength)) {
|
||||||
|
dueRepaymentPeriodDate = maxDateForFixedLength;
|
||||||
|
}
|
||||||
|
|
||||||
return dueRepaymentPeriodDate;
|
return dueRepaymentPeriodDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
@@ -22,6 +22,7 @@ import java.math.BigDecimal;
|
|||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import lombok.Getter;
|
||||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalcualtionAdditionalDetails;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalcualtionAdditionalDetails;
|
||||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData;
|
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData;
|
||||||
@@ -29,6 +30,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleP
|
|||||||
/**
|
/**
|
||||||
* Domain representation of a Loan Schedule Repayment Period (not used for persistence)
|
* Domain representation of a Loan Schedule Repayment Period (not used for persistence)
|
||||||
*/
|
*/
|
||||||
|
@Getter
|
||||||
public final class LoanScheduleModelRepaymentPeriod implements LoanScheduleModelPeriod {
|
public final class LoanScheduleModelRepaymentPeriod implements LoanScheduleModelPeriod {
|
||||||
|
|
||||||
private final int periodNumber;
|
private final int periodNumber;
|
||||||
|
|||||||
+2
-1
@@ -70,7 +70,8 @@ public final class CalculateLoanScheduleQueryFromApiJsonHelper {
|
|||||||
LoanApiConstants.interestRateDifferentialParameterName, LoanApiConstants.repaymentFrequencyNthDayTypeParameterName,
|
LoanApiConstants.interestRateDifferentialParameterName, LoanApiConstants.repaymentFrequencyNthDayTypeParameterName,
|
||||||
LoanApiConstants.repaymentFrequencyDayOfWeekTypeParameterName, LoanApiConstants.isTopup, LoanApiConstants.loanIdToClose,
|
LoanApiConstants.repaymentFrequencyDayOfWeekTypeParameterName, LoanApiConstants.isTopup, LoanApiConstants.loanIdToClose,
|
||||||
LoanApiConstants.datatables, LoanApiConstants.isEqualAmortizationParam, LoanProductConstants.RATES_PARAM_NAME,
|
LoanApiConstants.datatables, LoanApiConstants.isEqualAmortizationParam, LoanProductConstants.RATES_PARAM_NAME,
|
||||||
LoanApiConstants.daysInYearTypeParameterName, LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName));
|
LoanApiConstants.daysInYearTypeParameterName, LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName,
|
||||||
|
LoanProductConstants.FIXED_LENGTH));
|
||||||
|
|
||||||
private final FromJsonHelper fromApiJsonHelper;
|
private final FromJsonHelper fromApiJsonHelper;
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -196,6 +196,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
|
|||||||
private final LoanRepository loanRepository;
|
private final LoanRepository loanRepository;
|
||||||
private final GSIMReadPlatformService gsimReadPlatformService;
|
private final GSIMReadPlatformService gsimReadPlatformService;
|
||||||
private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine;
|
private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine;
|
||||||
|
private final LoanProductDataValidator loanProductDataValidator;
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
@@ -253,6 +254,9 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
|
|||||||
}
|
}
|
||||||
|
|
||||||
final Loan newLoanApplication = this.loanAssembler.assembleFrom(command);
|
final Loan newLoanApplication = this.loanAssembler.assembleFrom(command);
|
||||||
|
final LoanApplicationTerms loanApplicationTerms = this.loanScheduleAssembler.assembleLoanTerms(command.parsedJson());
|
||||||
|
loanProductDataValidator.fixedLengthValidations(newLoanApplication.getTransactionProcessingStrategyCode(), loanApplicationTerms,
|
||||||
|
this.fromJsonHelper.parse(command.json()), baseDataValidator);
|
||||||
|
|
||||||
checkForProductMixRestrictions(newLoanApplication);
|
checkForProductMixRestrictions(newLoanApplication);
|
||||||
|
|
||||||
@@ -438,7 +442,6 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
|
|||||||
CalendarEntityType.LOANS.getValue());
|
CalendarEntityType.LOANS.getValue());
|
||||||
this.calendarInstanceRepository.save(calendarInstance);
|
this.calendarInstanceRepository.save(calendarInstance);
|
||||||
} else {
|
} else {
|
||||||
final LoanApplicationTerms loanApplicationTerms = this.loanScheduleAssembler.assembleLoanTerms(command.parsedJson());
|
|
||||||
final Integer repaymentFrequencyNthDayType = command.integerValueOfParameterNamed("repaymentFrequencyNthDayType");
|
final Integer repaymentFrequencyNthDayType = command.integerValueOfParameterNamed("repaymentFrequencyNthDayType");
|
||||||
if (loanApplicationTerms.getRepaymentPeriodFrequencyType() == PeriodFrequencyType.MONTHS
|
if (loanApplicationTerms.getRepaymentPeriodFrequencyType() == PeriodFrequencyType.MONTHS
|
||||||
&& repaymentFrequencyNthDayType != null) {
|
&& repaymentFrequencyNthDayType != null) {
|
||||||
|
|||||||
+2
-2
@@ -231,7 +231,7 @@ public class LoanAccountConfiguration {
|
|||||||
|
|
||||||
RateAssembler rateAssembler, GLIMAccountInfoWritePlatformService glimAccountInfoWritePlatformService,
|
RateAssembler rateAssembler, GLIMAccountInfoWritePlatformService glimAccountInfoWritePlatformService,
|
||||||
GLIMAccountInfoRepository glimRepository, LoanRepository loanRepository, GSIMReadPlatformService gsimReadPlatformService,
|
GLIMAccountInfoRepository glimRepository, LoanRepository loanRepository, GSIMReadPlatformService gsimReadPlatformService,
|
||||||
LoanLifecycleStateMachine defaultLoanLifecycleStateMachine) {
|
LoanLifecycleStateMachine defaultLoanLifecycleStateMachine, LoanProductDataValidator loanProductDataValidator) {
|
||||||
return new LoanApplicationWritePlatformServiceJpaRepositoryImpl(context, fromJsonHelper, loanApplicationTransitionApiJsonValidator,
|
return new LoanApplicationWritePlatformServiceJpaRepositoryImpl(context, fromJsonHelper, loanApplicationTransitionApiJsonValidator,
|
||||||
loanProductCommandFromApiJsonDeserializer, fromApiJsonDeserializer, loanRepositoryWrapper, noteRepository,
|
loanProductCommandFromApiJsonDeserializer, fromApiJsonDeserializer, loanRepositoryWrapper, noteRepository,
|
||||||
calculationPlatformService, loanAssembler, clientRepository, loanProductRepository, loanChargeAssembler,
|
calculationPlatformService, loanAssembler, clientRepository, loanProductRepository, loanChargeAssembler,
|
||||||
@@ -241,7 +241,7 @@ public class LoanAccountConfiguration {
|
|||||||
configurationDomainService, loanScheduleAssembler, loanUtilService, calendarReadPlatformService,
|
configurationDomainService, loanScheduleAssembler, loanUtilService, calendarReadPlatformService,
|
||||||
entityDatatableChecksWritePlatformService, globalConfigurationRepository, entityMappingRepository,
|
entityDatatableChecksWritePlatformService, globalConfigurationRepository, entityMappingRepository,
|
||||||
fineractEntityRelationRepository, loanProductReadPlatformService, rateAssembler, glimAccountInfoWritePlatformService,
|
fineractEntityRelationRepository, loanProductReadPlatformService, rateAssembler, glimAccountInfoWritePlatformService,
|
||||||
glimRepository, loanRepository, gsimReadPlatformService, defaultLoanLifecycleStateMachine);
|
glimRepository, loanRepository, gsimReadPlatformService, defaultLoanLifecycleStateMachine, loanProductDataValidator);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
+60
-5
@@ -39,6 +39,7 @@ import org.apache.fineract.accounting.common.AccountingValidations;
|
|||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||||
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||||
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
|
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
|
||||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||||
@@ -47,6 +48,7 @@ import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
|
|||||||
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
|
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
|
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
|
||||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
|
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
|
||||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
|
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
|
||||||
import org.apache.fineract.portfolio.loanproduct.LoanProductConstants;
|
import org.apache.fineract.portfolio.loanproduct.LoanProductConstants;
|
||||||
@@ -399,6 +401,7 @@ public final class LoanProductDataValidator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// interest rates
|
// interest rates
|
||||||
|
BigDecimal interestRatePerPeriod = null;
|
||||||
if (this.fromApiJsonHelper.parameterExists(IS_LINKED_TO_FLOATING_INTEREST_RATES, element)
|
if (this.fromApiJsonHelper.parameterExists(IS_LINKED_TO_FLOATING_INTEREST_RATES, element)
|
||||||
&& this.fromApiJsonHelper.extractBooleanNamed(IS_LINKED_TO_FLOATING_INTEREST_RATES, element)) {
|
&& this.fromApiJsonHelper.extractBooleanNamed(IS_LINKED_TO_FLOATING_INTEREST_RATES, element)) {
|
||||||
if (isEqualAmortization) {
|
if (isEqualAmortization) {
|
||||||
@@ -519,8 +522,7 @@ public final class LoanProductDataValidator {
|
|||||||
"isFloatingInterestRateCalculationAllowed param is not supported when isLinkedToFloatingInterestRates is not supplied or false");
|
"isFloatingInterestRateCalculationAllowed param is not supported when isLinkedToFloatingInterestRates is not supplied or false");
|
||||||
}
|
}
|
||||||
|
|
||||||
final BigDecimal interestRatePerPeriod = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(INTEREST_RATE_PER_PERIOD,
|
interestRatePerPeriod = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(INTEREST_RATE_PER_PERIOD, element);
|
||||||
element);
|
|
||||||
baseDataValidator.reset().parameter(INTEREST_RATE_PER_PERIOD).value(interestRatePerPeriod).notNull().zeroOrPositiveAmount();
|
baseDataValidator.reset().parameter(INTEREST_RATE_PER_PERIOD).value(interestRatePerPeriod).notNull().zeroOrPositiveAmount();
|
||||||
|
|
||||||
final String minInterestRatePerPeriodParameterName = MIN_INTEREST_RATE_PER_PERIOD;
|
final String minInterestRatePerPeriodParameterName = MIN_INTEREST_RATE_PER_PERIOD;
|
||||||
@@ -564,6 +566,10 @@ public final class LoanProductDataValidator {
|
|||||||
4);
|
4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fixed Length validation
|
||||||
|
fixedLengthValidations(transactionProcessingStrategyCode, interestRatePerPeriod, numberOfRepayments, repaymentEvery, element,
|
||||||
|
baseDataValidator);
|
||||||
|
|
||||||
// Guarantee Funds
|
// Guarantee Funds
|
||||||
Boolean holdGuaranteeFunds = false;
|
Boolean holdGuaranteeFunds = false;
|
||||||
if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.holdGuaranteeFundsParamName, element)) {
|
if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.holdGuaranteeFundsParamName, element)) {
|
||||||
@@ -1238,13 +1244,15 @@ public final class LoanProductDataValidator {
|
|||||||
.integerGreaterThanZero();
|
.integerGreaterThanZero();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Integer numberOfRepayments = loanProduct.getNumberOfRepayments();
|
||||||
if (this.fromApiJsonHelper.parameterExists(NUMBER_OF_REPAYMENTS, element)) {
|
if (this.fromApiJsonHelper.parameterExists(NUMBER_OF_REPAYMENTS, element)) {
|
||||||
final Integer numberOfRepayments = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(NUMBER_OF_REPAYMENTS, element);
|
numberOfRepayments = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(NUMBER_OF_REPAYMENTS, element);
|
||||||
baseDataValidator.reset().parameter(NUMBER_OF_REPAYMENTS).value(numberOfRepayments).notNull().integerGreaterThanZero();
|
baseDataValidator.reset().parameter(NUMBER_OF_REPAYMENTS).value(numberOfRepayments).notNull().integerGreaterThanZero();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Integer repaymentEvery = loanProduct.getLoanProductRelatedDetail().getRepayEvery();
|
||||||
if (this.fromApiJsonHelper.parameterExists(REPAYMENT_EVERY, element)) {
|
if (this.fromApiJsonHelper.parameterExists(REPAYMENT_EVERY, element)) {
|
||||||
final Integer repaymentEvery = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(REPAYMENT_EVERY, element);
|
repaymentEvery = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(REPAYMENT_EVERY, element);
|
||||||
baseDataValidator.reset().parameter(REPAYMENT_EVERY).value(repaymentEvery).notNull().integerGreaterThanZero();
|
baseDataValidator.reset().parameter(REPAYMENT_EVERY).value(repaymentEvery).notNull().integerGreaterThanZero();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1364,6 +1372,7 @@ public final class LoanProductDataValidator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// interest rates
|
// interest rates
|
||||||
|
BigDecimal interestRatePerPeriod = null;
|
||||||
boolean isLinkedToFloatingInterestRates = loanProduct.isLinkedToFloatingInterestRate();
|
boolean isLinkedToFloatingInterestRates = loanProduct.isLinkedToFloatingInterestRate();
|
||||||
if (this.fromApiJsonHelper.parameterExists(IS_LINKED_TO_FLOATING_INTEREST_RATES, element)) {
|
if (this.fromApiJsonHelper.parameterExists(IS_LINKED_TO_FLOATING_INTEREST_RATES, element)) {
|
||||||
isLinkedToFloatingInterestRates = this.fromApiJsonHelper.extractBooleanNamed(IS_LINKED_TO_FLOATING_INTEREST_RATES, element);
|
isLinkedToFloatingInterestRates = this.fromApiJsonHelper.extractBooleanNamed(IS_LINKED_TO_FLOATING_INTEREST_RATES, element);
|
||||||
@@ -1531,7 +1540,7 @@ public final class LoanProductDataValidator {
|
|||||||
baseDataValidator.reset().parameter(maxInterestRatePerPeriodParameterName).value(maxInterestRatePerPeriod).ignoreIfNull()
|
baseDataValidator.reset().parameter(maxInterestRatePerPeriodParameterName).value(maxInterestRatePerPeriod).ignoreIfNull()
|
||||||
.zeroOrPositiveAmount();
|
.zeroOrPositiveAmount();
|
||||||
|
|
||||||
BigDecimal interestRatePerPeriod = loanProduct.getLoanProductRelatedDetail().getNominalInterestRatePerPeriod();
|
interestRatePerPeriod = loanProduct.getLoanProductRelatedDetail().getNominalInterestRatePerPeriod();
|
||||||
if (this.fromApiJsonHelper.parameterExists(INTEREST_RATE_PER_PERIOD, element)) {
|
if (this.fromApiJsonHelper.parameterExists(INTEREST_RATE_PER_PERIOD, element)) {
|
||||||
interestRatePerPeriod = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(INTEREST_RATE_PER_PERIOD, element);
|
interestRatePerPeriod = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(INTEREST_RATE_PER_PERIOD, element);
|
||||||
}
|
}
|
||||||
@@ -1546,6 +1555,10 @@ public final class LoanProductDataValidator {
|
|||||||
4);
|
4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fixed Length validation
|
||||||
|
fixedLengthValidations(transactionProcessingStrategyCode, interestRatePerPeriod, numberOfRepayments, repaymentEvery, element,
|
||||||
|
baseDataValidator);
|
||||||
|
|
||||||
// Guarantee Funds
|
// Guarantee Funds
|
||||||
Boolean holdGuaranteeFunds = loanProduct.isHoldGuaranteeFundsEnabled();
|
Boolean holdGuaranteeFunds = loanProduct.isHoldGuaranteeFundsEnabled();
|
||||||
if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.holdGuaranteeFundsParamName, element)) {
|
if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.holdGuaranteeFundsParamName, element)) {
|
||||||
@@ -2394,4 +2407,46 @@ public final class LoanProductDataValidator {
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void fixedLengthValidations(final String transactionProcessingStrategyCode, final LoanApplicationTerms loanApplicationTerms,
|
||||||
|
final JsonElement element, final DataValidatorBuilder baseDataValidator) {
|
||||||
|
fixedLengthValidations(transactionProcessingStrategyCode, loanApplicationTerms.getAnnualNominalInterestRate(),
|
||||||
|
loanApplicationTerms.getNumberOfRepayments(), loanApplicationTerms.getRepaymentEvery(), element, baseDataValidator);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void fixedLengthValidations(final String transactionProcessingStrategyCode, final BigDecimal interestRatePerPeriod,
|
||||||
|
final Integer numberOfRepayments, final Integer repayEvery, final JsonElement element,
|
||||||
|
final DataValidatorBuilder baseDataValidator) {
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.FIXED_LENGTH, element)) {
|
||||||
|
final JsonObject topLevelJsonElement = element.getAsJsonObject();
|
||||||
|
final Locale locale = this.fromApiJsonHelper.extractLocaleParameter(topLevelJsonElement);
|
||||||
|
final Integer fixedLength = this.fromApiJsonHelper.extractIntegerNamed(LoanProductConstants.FIXED_LENGTH, element, locale);
|
||||||
|
baseDataValidator.reset().parameter(LoanProductConstants.FIXED_LENGTH).value(fixedLength).ignoreIfNull()
|
||||||
|
.integerGreaterThanZero();
|
||||||
|
|
||||||
|
if (fixedLength != null) {
|
||||||
|
if (!AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY
|
||||||
|
.equals(transactionProcessingStrategyCode)) {
|
||||||
|
final String errorMsg = "Fixed Length configuration is only allowed with "
|
||||||
|
+ AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY + " stratefy";
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.fixed.length.only.supported.for.advanced.payment.allocation",
|
||||||
|
errorMsg, AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (interestRatePerPeriod.compareTo(BigDecimal.ZERO) > 0) {
|
||||||
|
final String errorMsg = "Fixed Length configuration is only allowed for zero interest products";
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.fixed.length.only.supported.for.zero.interest", errorMsg,
|
||||||
|
interestRatePerPeriod);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Integer valueToCompare = ((numberOfRepayments - 1) * repayEvery) + 1;
|
||||||
|
if (fixedLength.compareTo(valueToCompare) < 0) {
|
||||||
|
final String errorMsg = "Wrong configuration between Number Of Repayments: " + numberOfRepayments + " * " + repayEvery
|
||||||
|
+ " and Fixed Length: " + fixedLength + " values";
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.number.repayments.and.fixed.length.configuration.not.valid",
|
||||||
|
errorMsg, numberOfRepayments, repayEvery, valueToCompare, fixedLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+277
@@ -3542,6 +3542,283 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UC126: Advanced payment allocation with Fixed Length for 40 days and Loan Term for 45 days (3 repayments every 15
|
||||||
|
// days)
|
||||||
|
// ADVANCED_PAYMENT_ALLOCATION_STRATEGY
|
||||||
|
// 1. Create a Loan product with Adv. Pment. Alloc. and No Interest
|
||||||
|
// 2. Submit Loan and approve
|
||||||
|
// 3. Disburse
|
||||||
|
// 4. Validate Repayment Schedule
|
||||||
|
@Test
|
||||||
|
public void uc126() {
|
||||||
|
runAt("22 November 2023", () -> {
|
||||||
|
final Integer fixedLength = 40; // 40 days
|
||||||
|
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
|
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
|
||||||
|
.numberOfRepayments(3).repaymentEvery(15).fixedLength(fixedLength);
|
||||||
|
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
|
||||||
|
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), "22 November 2023",
|
||||||
|
1000.0, 4);
|
||||||
|
|
||||||
|
applicationRequest = applicationRequest.numberOfRepayments(3).loanTermFrequency(45)
|
||||||
|
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY).repaymentEvery(15);
|
||||||
|
|
||||||
|
PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest);
|
||||||
|
|
||||||
|
loanTransactionHelper.approveLoan(loanResponse.getLoanId(),
|
||||||
|
new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000)).dateFormat(DATETIME_PATTERN)
|
||||||
|
.approvedOnDate("22 November 2023").locale("en"));
|
||||||
|
|
||||||
|
loanTransactionHelper.disburseLoan(loanResponse.getLoanId(),
|
||||||
|
new PostLoansLoanIdRequest().actualDisbursementDate("22 November 2023").dateFormat(DATETIME_PATTERN)
|
||||||
|
.transactionAmount(BigDecimal.valueOf(100.0)).locale("en"));
|
||||||
|
|
||||||
|
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
|
||||||
|
validateLoanSummaryBalances(loanDetails, 100.0, 0.0, 100.0, 0.0, null);
|
||||||
|
validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 12, 7), 33.0, 0.0, 33.0, 0.0, 0.0);
|
||||||
|
validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 12, 22), 33.0, 0.0, 33.0, 0.0, 0.0);
|
||||||
|
validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2024, 1, 01), 34.0, 0.0, 34.0, 0.0, 0.0);
|
||||||
|
assertEquals(loanDetails.getNumberOfRepayments(), 3);
|
||||||
|
assertEquals(
|
||||||
|
Utils.getDifferenceInDays(loanDetails.getTimeline().getActualDisbursementDate(), loanDetails.getRepaymentSchedule()
|
||||||
|
.getPeriods().get(loanDetails.getRepaymentSchedule().getPeriods().size() - 1).getDueDate()),
|
||||||
|
fixedLength.longValue());
|
||||||
|
assertTrue(loanDetails.getStatus().getActive());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// UC127: Advanced payment allocation with Fixed Length for 5 weeks and Loan Term for 6 weeks (3 repayments every 2
|
||||||
|
// weeks)
|
||||||
|
// ADVANCED_PAYMENT_ALLOCATION_STRATEGY
|
||||||
|
// 1. Create a Loan product with Adv. Pment. Alloc. and No Interest
|
||||||
|
// 2. Submit Loan and approve
|
||||||
|
// 3. Disburse
|
||||||
|
// 4. Validate Repayment Schedule
|
||||||
|
@Test
|
||||||
|
public void uc127() {
|
||||||
|
runAt("22 November 2023", () -> {
|
||||||
|
final Integer fixedLength = 5; // 5 weeks
|
||||||
|
final Integer repaymentFrequencyType = 1; // week
|
||||||
|
|
||||||
|
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
|
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
|
||||||
|
.numberOfRepayments(3).repaymentEvery(2).repaymentFrequencyType(repaymentFrequencyType.longValue())
|
||||||
|
.fixedLength(fixedLength);
|
||||||
|
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
|
||||||
|
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), "22 November 2023",
|
||||||
|
1000.0, 4);
|
||||||
|
|
||||||
|
applicationRequest = applicationRequest.numberOfRepayments(3).loanTermFrequency(6).loanTermFrequencyType(repaymentFrequencyType)
|
||||||
|
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY).repaymentEvery(2)
|
||||||
|
.repaymentFrequencyType(repaymentFrequencyType);
|
||||||
|
|
||||||
|
PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest);
|
||||||
|
|
||||||
|
loanTransactionHelper.approveLoan(loanResponse.getLoanId(),
|
||||||
|
new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000)).dateFormat(DATETIME_PATTERN)
|
||||||
|
.approvedOnDate("22 November 2023").locale("en"));
|
||||||
|
|
||||||
|
loanTransactionHelper.disburseLoan(loanResponse.getLoanId(),
|
||||||
|
new PostLoansLoanIdRequest().actualDisbursementDate("22 November 2023").dateFormat(DATETIME_PATTERN)
|
||||||
|
.transactionAmount(BigDecimal.valueOf(100.0)).locale("en"));
|
||||||
|
|
||||||
|
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
|
||||||
|
validateLoanSummaryBalances(loanDetails, 100.0, 0.0, 100.0, 0.0, null);
|
||||||
|
validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 12, 6), 33.0, 0.0, 33.0, 0.0, 0.0);
|
||||||
|
validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 12, 20), 33.0, 0.0, 33.0, 0.0, 0.0);
|
||||||
|
validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 12, 27), 34.0, 0.0, 34.0, 0.0, 0.0);
|
||||||
|
assertEquals(loanDetails.getNumberOfRepayments(), 3);
|
||||||
|
assertEquals(
|
||||||
|
Utils.getDifferenceInWeeks(loanDetails.getTimeline().getActualDisbursementDate(), loanDetails.getRepaymentSchedule()
|
||||||
|
.getPeriods().get(loanDetails.getRepaymentSchedule().getPeriods().size() - 1).getDueDate()),
|
||||||
|
fixedLength.longValue());
|
||||||
|
assertTrue(loanDetails.getStatus().getActive());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// UC128: Advanced payment allocation with Fixed Length for 11 months and Loan Term for 12 months (6 repayments each
|
||||||
|
// 2 months)
|
||||||
|
// every 2 months)
|
||||||
|
// ADVANCED_PAYMENT_ALLOCATION_STRATEGY
|
||||||
|
// 1. Create a Loan product with Adv. Pment. Alloc. and No Interest
|
||||||
|
// 2. Submit Loan and approve
|
||||||
|
// 3. Disburse
|
||||||
|
// 4. Validate Repayment Schedule
|
||||||
|
@Test
|
||||||
|
public void uc128() {
|
||||||
|
runAt("22 November 2023", () -> {
|
||||||
|
final Integer fixedLength = 11; // 11 months
|
||||||
|
final Integer repaymentFrequencyType = 2; // month
|
||||||
|
|
||||||
|
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
|
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
|
||||||
|
.numberOfRepayments(6).repaymentEvery(2).repaymentFrequencyType(repaymentFrequencyType.longValue())
|
||||||
|
.fixedLength(fixedLength);
|
||||||
|
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
|
||||||
|
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), "22 November 2023",
|
||||||
|
1000.0, 4);
|
||||||
|
|
||||||
|
applicationRequest = applicationRequest.numberOfRepayments(6).loanTermFrequency(12)
|
||||||
|
.loanTermFrequencyType(repaymentFrequencyType)
|
||||||
|
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY).repaymentEvery(2)
|
||||||
|
.repaymentFrequencyType(repaymentFrequencyType);
|
||||||
|
|
||||||
|
PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest);
|
||||||
|
|
||||||
|
loanTransactionHelper.approveLoan(loanResponse.getLoanId(),
|
||||||
|
new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000)).dateFormat(DATETIME_PATTERN)
|
||||||
|
.approvedOnDate("22 November 2023").locale("en"));
|
||||||
|
|
||||||
|
loanTransactionHelper.disburseLoan(loanResponse.getLoanId(),
|
||||||
|
new PostLoansLoanIdRequest().actualDisbursementDate("22 November 2023").dateFormat(DATETIME_PATTERN)
|
||||||
|
.transactionAmount(BigDecimal.valueOf(100.0)).locale("en"));
|
||||||
|
|
||||||
|
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
|
||||||
|
validateLoanSummaryBalances(loanDetails, 100.0, 0.0, 100.0, 0.0, null);
|
||||||
|
validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2024, 1, 22), 17.0, 0.0, 17.0, 0.0, 0.0);
|
||||||
|
validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2024, 3, 22), 17.0, 0.0, 17.0, 0.0, 0.0);
|
||||||
|
validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2024, 5, 22), 17.0, 0.0, 17.0, 0.0, 0.0);
|
||||||
|
validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2024, 7, 22), 17.0, 0.0, 17.0, 0.0, 0.0);
|
||||||
|
validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2024, 9, 22), 17.0, 0.0, 17.0, 0.0, 0.0);
|
||||||
|
validateRepaymentPeriod(loanDetails, 6, LocalDate.of(2024, 10, 22), 15.0, 0.0, 15.0, 0.0, 0.0);
|
||||||
|
assertEquals(loanDetails.getNumberOfRepayments(), 6);
|
||||||
|
assertEquals(
|
||||||
|
Utils.getDifferenceInMonths(loanDetails.getTimeline().getActualDisbursementDate(), loanDetails.getRepaymentSchedule()
|
||||||
|
.getPeriods().get(loanDetails.getRepaymentSchedule().getPeriods().size() - 1).getDueDate()),
|
||||||
|
fixedLength.longValue());
|
||||||
|
assertTrue(loanDetails.getStatus().getActive());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// UC129: Advanced payment allocation with Fixed Length for 5 months and Loan Term for 6 months (6 repayments
|
||||||
|
// every month)
|
||||||
|
// ADVANCED_PAYMENT_ALLOCATION_STRATEGY
|
||||||
|
// 1. Create a Loan product with Adv. Pment. Alloc. and No Interest
|
||||||
|
// 2. Submit Loan and approve -> expect validation error
|
||||||
|
@Test
|
||||||
|
public void uc129() {
|
||||||
|
final String operationDate = "22 November 2023";
|
||||||
|
runAt(operationDate, () -> {
|
||||||
|
final Integer fixedLength = 5; // 5 months
|
||||||
|
final Integer repaymentFrequencyType = 2; // month
|
||||||
|
|
||||||
|
// Try to create a Loan Product using Fixed Length with a wrong Loan Term setup
|
||||||
|
LOG.info("Try to create a Loan Product using Fixed Length with a wrong Loan Term setup");
|
||||||
|
CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class,
|
||||||
|
() -> loanProductHelper
|
||||||
|
.createLoanProduct(createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
|
||||||
|
.interestRatePerPeriod(0.0).numberOfRepayments(6).repaymentEvery(1)
|
||||||
|
.repaymentFrequencyType(repaymentFrequencyType.longValue()).fixedLength(fixedLength)));
|
||||||
|
assertEquals(403, exception.getResponse().code());
|
||||||
|
assertTrue(exception.getMessage().contains("error.msg.number.repayments.and.fixed.length.configuration.not.valid"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// UC130: Advanced payment allocation with Fixed Length for 5 months and Loan Term for 6 months (6 repayments
|
||||||
|
// every month)
|
||||||
|
// ADVANCED_PAYMENT_ALLOCATION_STRATEGY
|
||||||
|
// 1. Create a Loan product with Adv. Pment. Alloc. and No Interest
|
||||||
|
// 2. Submit Loan and approve -> expect validation error - Fixed Length without Advanced Payment Allocation
|
||||||
|
@Test
|
||||||
|
public void uc130() {
|
||||||
|
final String operationDate = "22 November 2023";
|
||||||
|
runAt(operationDate, () -> {
|
||||||
|
final Integer fixedLength = 5; // 5 months
|
||||||
|
final Integer repaymentFrequencyType = 2; // month
|
||||||
|
|
||||||
|
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
|
||||||
|
.numberOfRepayments(6).repaymentEvery(1).repaymentFrequencyType(repaymentFrequencyType.longValue()).fixedLength(6);
|
||||||
|
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
|
||||||
|
|
||||||
|
// Try to use Fixed Length without Advanced Payment Allocation as transaction processing strategy code
|
||||||
|
LOG.info("Try to use Fixed Length without Advanced Payment Allocation as transaction processing strategy code");
|
||||||
|
CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class,
|
||||||
|
() -> loanTransactionHelper.applyLoan(new PostLoansRequest().clientId(client.getClientId())
|
||||||
|
.productId(loanProductResponse.getResourceId()).loanType("individual").locale("en")
|
||||||
|
.submittedOnDate(operationDate).expectedDisbursementDate(operationDate).dateFormat(DATETIME_PATTERN)
|
||||||
|
.amortizationType(1).interestRatePerPeriod(BigDecimal.ZERO).interestCalculationPeriodType(1).interestType(0)
|
||||||
|
.repaymentEvery(1).repaymentFrequencyType(repaymentFrequencyType).numberOfRepayments(6).loanTermFrequency(6)
|
||||||
|
.loanTermFrequencyType(repaymentFrequencyType).principal(BigDecimal.valueOf(1000.0))
|
||||||
|
.maxOutstandingLoanBalance(BigDecimal.valueOf(35000)).fixedLength(fixedLength)
|
||||||
|
.transactionProcessingStrategyCode(FineractStyleLoanRepaymentScheduleTransactionProcessor.STRATEGY_CODE)
|
||||||
|
.loanScheduleProcessingType(LoanScheduleProcessingType.HORIZONTAL.name())));
|
||||||
|
|
||||||
|
assertEquals(403, exception.getResponse().code());
|
||||||
|
assertTrue(exception.getMessage()
|
||||||
|
.contains("error.msg.loan.repayment.strategy.can.not.be.different.than.advanced.payment.allocation"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// UC131: Advanced payment allocation with Fixed Length for 5 months and Loan Term for 6 months (6 repayments
|
||||||
|
// every month)
|
||||||
|
// ADVANCED_PAYMENT_ALLOCATION_STRATEGY
|
||||||
|
// 1. Create a Loan product with Adv. Pment. Alloc. and No Interest
|
||||||
|
// 2. Submit Loan and approve -> expect validation error - Interest Rate value higher than Zero
|
||||||
|
@Test
|
||||||
|
public void uc131() {
|
||||||
|
final String operationDate = "22 November 2023";
|
||||||
|
runAt(operationDate, () -> {
|
||||||
|
final Integer fixedLength = 5; // 5 months
|
||||||
|
final Integer repaymentFrequencyType = 2; // month
|
||||||
|
|
||||||
|
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
|
||||||
|
.numberOfRepayments(6).repaymentEvery(1).repaymentFrequencyType(repaymentFrequencyType.longValue()).fixedLength(6);
|
||||||
|
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
|
||||||
|
|
||||||
|
// Try to use Fixed Length with an Interest Rate value higher than Zero
|
||||||
|
LOG.info("Try to use Fixed Length with an Interest Rate value higher than Zero");
|
||||||
|
CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class,
|
||||||
|
() -> loanTransactionHelper.applyLoan(new PostLoansRequest().clientId(client.getClientId())
|
||||||
|
.productId(loanProductResponse.getResourceId()).loanType("individual").locale("en")
|
||||||
|
.submittedOnDate(operationDate).expectedDisbursementDate(operationDate).dateFormat(DATETIME_PATTERN)
|
||||||
|
.amortizationType(1).interestRatePerPeriod(BigDecimal.ONE).interestCalculationPeriodType(1).interestType(0)
|
||||||
|
.repaymentEvery(1).repaymentFrequencyType(repaymentFrequencyType).numberOfRepayments(6).loanTermFrequency(6)
|
||||||
|
.loanTermFrequencyType(repaymentFrequencyType).principal(BigDecimal.valueOf(1000.0))
|
||||||
|
.maxOutstandingLoanBalance(BigDecimal.valueOf(35000)).fixedLength(fixedLength)
|
||||||
|
.transactionProcessingStrategyCode(
|
||||||
|
AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)
|
||||||
|
.loanScheduleProcessingType(LoanScheduleProcessingType.HORIZONTAL.name())));
|
||||||
|
|
||||||
|
assertEquals(403, exception.getResponse().code());
|
||||||
|
assertTrue(exception.getMessage().contains("error.msg.fixed.length.only.supported.for.zero.interest"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// UC132: Advanced payment allocation with Fixed Length for 5 months and Loan Term for 6 months (3 repayments
|
||||||
|
// every 2 months)
|
||||||
|
// ADVANCED_PAYMENT_ALLOCATION_STRATEGY
|
||||||
|
// 1. Create a Loan product with Adv. Pment. Alloc. and No Interest
|
||||||
|
// 2. Submit Loan and approve -> expect validation error - Loan acount Fixed Length with a wrong Loan Term setup
|
||||||
|
@Test
|
||||||
|
public void uc132() {
|
||||||
|
final String operationDate = "22 November 2023";
|
||||||
|
runAt(operationDate, () -> {
|
||||||
|
final Integer fixedLength = 5; // 5 months
|
||||||
|
final Integer repaymentFrequencyType = 2; // month
|
||||||
|
|
||||||
|
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
|
||||||
|
.numberOfRepayments(6).repaymentEvery(1).repaymentFrequencyType(repaymentFrequencyType.longValue()).fixedLength(6);
|
||||||
|
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
|
||||||
|
|
||||||
|
// Try to use Fixed Length with a wrong Loan Term setup
|
||||||
|
LOG.info("Try to use Fixed Length with a wrong Loan Term setup");
|
||||||
|
CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class,
|
||||||
|
() -> loanTransactionHelper.applyLoan(new PostLoansRequest().clientId(client.getClientId())
|
||||||
|
.productId(loanProductResponse.getResourceId()).loanType("individual").locale("en")
|
||||||
|
.submittedOnDate(operationDate).expectedDisbursementDate(operationDate).dateFormat(DATETIME_PATTERN)
|
||||||
|
.amortizationType(1).interestRatePerPeriod(BigDecimal.ZERO).interestCalculationPeriodType(1).interestType(0)
|
||||||
|
.repaymentEvery(2).repaymentFrequencyType(repaymentFrequencyType).numberOfRepayments(6).loanTermFrequency(6)
|
||||||
|
.loanTermFrequencyType(repaymentFrequencyType).principal(BigDecimal.valueOf(1000.0))
|
||||||
|
.maxOutstandingLoanBalance(BigDecimal.valueOf(35000)).fixedLength(fixedLength)
|
||||||
|
.transactionProcessingStrategyCode(
|
||||||
|
AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)
|
||||||
|
.loanScheduleProcessingType(LoanScheduleProcessingType.HORIZONTAL.name())));
|
||||||
|
|
||||||
|
assertEquals(403, exception.getResponse().code());
|
||||||
|
assertTrue(exception.getMessage().contains("error.msg.number.repayments.and.fixed.length.configuration.not.valid"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private static List<PaymentAllocationOrder> getPaymentAllocationOrder(PaymentAllocationType... paymentAllocationTypes) {
|
private static List<PaymentAllocationOrder> getPaymentAllocationOrder(PaymentAllocationType... paymentAllocationTypes) {
|
||||||
AtomicInteger integer = new AtomicInteger(1);
|
AtomicInteger integer = new AtomicInteger(1);
|
||||||
return Arrays.stream(paymentAllocationTypes).map(pat -> {
|
return Arrays.stream(paymentAllocationTypes).map(pat -> {
|
||||||
|
|||||||
+36
-17
@@ -22,10 +22,16 @@ import org.apache.fineract.client.models.GetLoanProductsProductIdResponse;
|
|||||||
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
|
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
|
||||||
import org.apache.fineract.client.models.PostLoanProductsRequest;
|
import org.apache.fineract.client.models.PostLoanProductsRequest;
|
||||||
import org.apache.fineract.client.models.PostLoanProductsResponse;
|
import org.apache.fineract.client.models.PostLoanProductsResponse;
|
||||||
|
import org.apache.fineract.client.models.PostLoansRequest;
|
||||||
|
import org.apache.fineract.client.models.PostLoansResponse;
|
||||||
import org.apache.fineract.client.models.PutLoanProductsProductIdRequest;
|
import org.apache.fineract.client.models.PutLoanProductsProductIdRequest;
|
||||||
import org.apache.fineract.client.models.PutLoanProductsProductIdResponse;
|
import org.apache.fineract.client.models.PutLoanProductsProductIdResponse;
|
||||||
import org.apache.fineract.integrationtests.common.ClientHelper;
|
import org.apache.fineract.integrationtests.common.ClientHelper;
|
||||||
|
import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
|
||||||
import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension;
|
import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
|
||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
@@ -35,25 +41,25 @@ public class FixedLengthLoanProductIntegrationTest extends BaseLoanIntegrationTe
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testCreateReadUpdateReadLoanProductWithFixedLength() {
|
public void testCreateReadUpdateReadLoanProductWithFixedLength() {
|
||||||
// create with 5
|
// create with 4
|
||||||
PostLoanProductsRequest loanProductsRequest = fixedLengthLoanProduct(5);
|
PostLoanProductsRequest loanProductsRequest = fixedLengthLoanProduct(4);
|
||||||
PostLoanProductsResponse loanProduct = loanProductHelper.createLoanProduct(loanProductsRequest);
|
PostLoanProductsResponse loanProduct = loanProductHelper.createLoanProduct(loanProductsRequest);
|
||||||
Assertions.assertNotNull(loanProduct.getResourceId());
|
Assertions.assertNotNull(loanProduct.getResourceId());
|
||||||
|
|
||||||
// read
|
// read
|
||||||
GetLoanProductsProductIdResponse getLoanProductsProductIdResponse = loanProductHelper
|
GetLoanProductsProductIdResponse getLoanProductsProductIdResponse = loanProductHelper
|
||||||
.retrieveLoanProductById(loanProduct.getResourceId());
|
.retrieveLoanProductById(loanProduct.getResourceId());
|
||||||
Assertions.assertEquals(5, getLoanProductsProductIdResponse.getFixedLength());
|
Assertions.assertEquals(4, getLoanProductsProductIdResponse.getFixedLength());
|
||||||
|
|
||||||
// update to 6
|
// update to 5
|
||||||
PutLoanProductsProductIdRequest updateRequest = new PutLoanProductsProductIdRequest().fixedLength(6).locale("en");
|
PutLoanProductsProductIdRequest updateRequest = new PutLoanProductsProductIdRequest().fixedLength(5).locale("en");
|
||||||
PutLoanProductsProductIdResponse putLoanProductsProductIdResponse = loanProductHelper
|
PutLoanProductsProductIdResponse putLoanProductsProductIdResponse = loanProductHelper
|
||||||
.updateLoanProductById(loanProduct.getResourceId(), updateRequest);
|
.updateLoanProductById(loanProduct.getResourceId(), updateRequest);
|
||||||
Assertions.assertNotNull(putLoanProductsProductIdResponse.getResourceId());
|
Assertions.assertNotNull(putLoanProductsProductIdResponse.getResourceId());
|
||||||
|
|
||||||
// read again
|
// read again
|
||||||
getLoanProductsProductIdResponse = loanProductHelper.retrieveLoanProductById(loanProduct.getResourceId());
|
getLoanProductsProductIdResponse = loanProductHelper.retrieveLoanProductById(loanProduct.getResourceId());
|
||||||
Assertions.assertEquals(6, getLoanProductsProductIdResponse.getFixedLength());
|
Assertions.assertEquals(5, getLoanProductsProductIdResponse.getFixedLength());
|
||||||
|
|
||||||
// update to null
|
// update to null
|
||||||
loanTransactionHelper.updateLoanProduct(putLoanProductsProductIdResponse.getResourceId(), """
|
loanTransactionHelper.updateLoanProduct(putLoanProductsProductIdResponse.getResourceId(), """
|
||||||
@@ -73,12 +79,18 @@ public class FixedLengthLoanProductIntegrationTest extends BaseLoanIntegrationTe
|
|||||||
runAt("01 January 2023", () -> {
|
runAt("01 January 2023", () -> {
|
||||||
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
|
|
||||||
PostLoanProductsResponse loanProduct = loanProductHelper.createLoanProduct(fixedLengthLoanProduct(6));
|
PostLoanProductsResponse loanProduct = loanProductHelper.createLoanProduct(fixedLengthLoanProduct(4));
|
||||||
Assertions.assertNotNull(loanProduct.getResourceId());
|
Assertions.assertNotNull(loanProduct.getResourceId());
|
||||||
|
|
||||||
Long loanId = applyAndApproveLoan(clientId, loanProduct.getResourceId(), "01 January 2023", 1000.0);
|
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProduct.getResourceId(), "01 January 2023", 1000.0, 4);
|
||||||
|
applicationRequest = applicationRequest
|
||||||
|
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY);
|
||||||
|
|
||||||
|
PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest);
|
||||||
|
Long loanId = loanResponse.getLoanId();
|
||||||
|
|
||||||
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
||||||
Assertions.assertEquals(6, loanDetails.getFixedLength());
|
Assertions.assertEquals(4, loanDetails.getFixedLength());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,22 +99,29 @@ public class FixedLengthLoanProductIntegrationTest extends BaseLoanIntegrationTe
|
|||||||
runAt("01 January 2023", () -> {
|
runAt("01 January 2023", () -> {
|
||||||
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
|
|
||||||
PostLoanProductsResponse loanProduct = loanProductHelper.createLoanProduct(fixedLengthLoanProduct(6));
|
PostLoanProductsResponse loanProduct = loanProductHelper.createLoanProduct(fixedLengthLoanProduct(4));
|
||||||
Assertions.assertNotNull(loanProduct.getResourceId());
|
Assertions.assertNotNull(loanProduct.getResourceId());
|
||||||
|
|
||||||
Long loanId = applyAndApproveLoan(clientId, loanProduct.getResourceId(), "01 January 2023", 1000.0, 1, //
|
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProduct.getResourceId(), "01 January 2023", 1000.0, 4);
|
||||||
loanApplication -> loanApplication.fixedLength(5) //
|
applicationRequest = applicationRequest.fixedLength(5).repaymentEvery(1).repaymentFrequencyType(2).loanTermFrequencyType(2)
|
||||||
);
|
.loanTermFrequency(4).transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY);
|
||||||
|
|
||||||
|
PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest);
|
||||||
|
Long loanId = loanResponse.getLoanId();
|
||||||
|
|
||||||
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
||||||
Assertions.assertEquals(5, loanDetails.getFixedLength());
|
Assertions.assertEquals(5, loanDetails.getFixedLength());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private PostLoanProductsRequest fixedLengthLoanProduct(Integer fixedLength) {
|
private PostLoanProductsRequest fixedLengthLoanProduct(Integer fixedLength) {
|
||||||
return createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct().numberOfRepayments(4)//
|
return createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation() //
|
||||||
.repaymentEvery(1)//
|
.numberOfRepayments(4).repaymentEvery(1) //
|
||||||
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue())//
|
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()) //
|
||||||
.transactionProcessingStrategyCode("mifos-standard-strategy").fixedLength(fixedLength);
|
.loanScheduleType(LoanScheduleType.PROGRESSIVE.toString()) //
|
||||||
|
.transactionProcessingStrategyCode(AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY) //
|
||||||
|
.loanScheduleProcessingType(LoanScheduleProcessingType.HORIZONTAL.name()) //
|
||||||
|
.interestRatePerPeriod(0.0).fixedLength(fixedLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+15
@@ -19,6 +19,9 @@
|
|||||||
package org.apache.fineract.integrationtests.common;
|
package org.apache.fineract.integrationtests.common;
|
||||||
|
|
||||||
import static io.restassured.RestAssured.given;
|
import static io.restassured.RestAssured.given;
|
||||||
|
import static java.time.temporal.ChronoUnit.DAYS;
|
||||||
|
import static java.time.temporal.ChronoUnit.MONTHS;
|
||||||
|
import static java.time.temporal.ChronoUnit.WEEKS;
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
import static org.hamcrest.Matchers.is;
|
import static org.hamcrest.Matchers.is;
|
||||||
import static org.junit.jupiter.api.Assertions.fail;
|
import static org.junit.jupiter.api.Assertions.fail;
|
||||||
@@ -520,4 +523,16 @@ public final class Utils {
|
|||||||
public static LocalDate getDateAsLocalDate(String dateAsString) {
|
public static LocalDate getDateAsLocalDate(String dateAsString) {
|
||||||
return LocalDate.parse(dateAsString, dateFormatter);
|
return LocalDate.parse(dateAsString, dateFormatter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static long getDifferenceInDays(final LocalDate localDateBefore, final LocalDate localDateAfter) {
|
||||||
|
return DAYS.between(localDateBefore, localDateAfter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long getDifferenceInWeeks(final LocalDate localDateBefore, final LocalDate localDateAfter) {
|
||||||
|
return WEEKS.between(localDateBefore, localDateAfter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long getDifferenceInMonths(final LocalDate localDateBefore, final LocalDate localDateAfter) {
|
||||||
|
return MONTHS.between(localDateBefore, localDateAfter);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
@@ -343,6 +343,11 @@ public class LoanTransactionHelper extends IntegrationTest {
|
|||||||
return ok(fineract().loanCharges.retrieveTemplate9(loanExternalId));
|
return ok(fineract().loanCharges.retrieveTemplate9(loanExternalId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public HashMap applyLoan(final String payload, final ResponseSpecification responseSpec) {
|
||||||
|
final String postURLForLoan = "/fineract-provider/api/v1/loans?" + Utils.TENANT_IDENTIFIER;
|
||||||
|
return Utils.performServerPost(this.requestSpec, this.responseSpec, postURLForLoan, payload, null);
|
||||||
|
}
|
||||||
|
|
||||||
public List getRepaymentTemplate(final Integer loanId) {
|
public List getRepaymentTemplate(final Integer loanId) {
|
||||||
final String GET_REPAYMENTS_URL = "/fineract-provider/api/v1/loans/" + loanId + "/transactions/template?command=repayment&"
|
final String GET_REPAYMENTS_URL = "/fineract-provider/api/v1/loans/" + loanId + "/transactions/template?command=repayment&"
|
||||||
+ Utils.TENANT_IDENTIFIER;
|
+ Utils.TENANT_IDENTIFIER;
|
||||||
|
|||||||
Reference in New Issue
Block a user