FINERACT-2065: Schedule handling for fixed length configuration

This commit is contained in:
Jose Alberto Hernandez
2024-03-27 12:54:26 +01:00
committed by Adam Saghy
parent 6458094a5e
commit b64b4b7105
11 changed files with 453 additions and 32 deletions
@@ -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.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType;
import org.springframework.stereotype.Component;
@Component
@@ -46,16 +45,16 @@ public class DefaultScheduledDateGenerator implements ScheduledDateGenerator {
final int numberOfRepayments = loanApplicationTerms.getNumberOfRepayments();
RepaymentStartDateType repaymentStartDateType = loanApplicationTerms.getRepaymentStartDateType();
LocalDate lastRepaymentDate = RepaymentStartDateType.DISBURSEMENT_DATE.equals(repaymentStartDateType)
? loanApplicationTerms.getExpectedDisbursementDate()
: loanApplicationTerms.getSubmittedOnDate();
LocalDate lastRepaymentDate = loanApplicationTerms.getRepaymentStartDate();
boolean isFirstRepayment = true;
for (int repaymentPeriod = 1; repaymentPeriod <= numberOfRepayments; repaymentPeriod++) {
lastRepaymentDate = generateNextRepaymentDate(lastRepaymentDate, loanApplicationTerms, isFirstRepayment);
isFirstRepayment = false;
}
if (loanApplicationTerms.getFixedLength() != null
&& loanApplicationTerms.calculateMaxDateForFixedLength().compareTo(lastRepaymentDate) != 0) {
lastRepaymentDate = loanApplicationTerms.calculateMaxDateForFixedLength();
}
lastRepaymentDate = adjustRepaymentDate(lastRepaymentDate, loanApplicationTerms, holidayDetailDTO).getChangedScheduleDate();
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;
}
@@ -22,6 +22,7 @@ import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.HashSet;
import java.util.Set;
import lombok.Getter;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalcualtionAdditionalDetails;
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)
*/
@Getter
public final class LoanScheduleModelRepaymentPeriod implements LoanScheduleModelPeriod {
private final int periodNumber;
@@ -70,7 +70,8 @@ public final class CalculateLoanScheduleQueryFromApiJsonHelper {
LoanApiConstants.interestRateDifferentialParameterName, LoanApiConstants.repaymentFrequencyNthDayTypeParameterName,
LoanApiConstants.repaymentFrequencyDayOfWeekTypeParameterName, LoanApiConstants.isTopup, LoanApiConstants.loanIdToClose,
LoanApiConstants.datatables, LoanApiConstants.isEqualAmortizationParam, LoanProductConstants.RATES_PARAM_NAME,
LoanApiConstants.daysInYearTypeParameterName, LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName));
LoanApiConstants.daysInYearTypeParameterName, LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName,
LoanProductConstants.FIXED_LENGTH));
private final FromJsonHelper fromApiJsonHelper;
@@ -196,6 +196,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
private final LoanRepository loanRepository;
private final GSIMReadPlatformService gsimReadPlatformService;
private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine;
private final LoanProductDataValidator loanProductDataValidator;
@Transactional
@Override
@@ -253,6 +254,9 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
}
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);
@@ -438,7 +442,6 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
CalendarEntityType.LOANS.getValue());
this.calendarInstanceRepository.save(calendarInstance);
} else {
final LoanApplicationTerms loanApplicationTerms = this.loanScheduleAssembler.assembleLoanTerms(command.parsedJson());
final Integer repaymentFrequencyNthDayType = command.integerValueOfParameterNamed("repaymentFrequencyNthDayType");
if (loanApplicationTerms.getRepaymentPeriodFrequencyType() == PeriodFrequencyType.MONTHS
&& repaymentFrequencyNthDayType != null) {
@@ -231,7 +231,7 @@ public class LoanAccountConfiguration {
RateAssembler rateAssembler, GLIMAccountInfoWritePlatformService glimAccountInfoWritePlatformService,
GLIMAccountInfoRepository glimRepository, LoanRepository loanRepository, GSIMReadPlatformService gsimReadPlatformService,
LoanLifecycleStateMachine defaultLoanLifecycleStateMachine) {
LoanLifecycleStateMachine defaultLoanLifecycleStateMachine, LoanProductDataValidator loanProductDataValidator) {
return new LoanApplicationWritePlatformServiceJpaRepositoryImpl(context, fromJsonHelper, loanApplicationTransitionApiJsonValidator,
loanProductCommandFromApiJsonDeserializer, fromApiJsonDeserializer, loanRepositoryWrapper, noteRepository,
calculationPlatformService, loanAssembler, clientRepository, loanProductRepository, loanChargeAssembler,
@@ -241,7 +241,7 @@ public class LoanAccountConfiguration {
configurationDomainService, loanScheduleAssembler, loanUtilService, calendarReadPlatformService,
entityDatatableChecksWritePlatformService, globalConfigurationRepository, entityMappingRepository,
fineractEntityRelationRepository, loanProductReadPlatformService, rateAssembler, glimAccountInfoWritePlatformService,
glimRepository, loanRepository, gsimReadPlatformService, defaultLoanLifecycleStateMachine);
glimRepository, loanRepository, gsimReadPlatformService, defaultLoanLifecycleStateMachine, loanProductDataValidator);
}
@Bean
@@ -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.data.ApiParameterError;
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.PlatformApiDataValidationException;
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.domain.LoanRepaymentScheduleTransactionProcessorFactory;
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.LoanScheduleType;
import org.apache.fineract.portfolio.loanproduct.LoanProductConstants;
@@ -399,6 +401,7 @@ public final class LoanProductDataValidator {
}
// interest rates
BigDecimal interestRatePerPeriod = null;
if (this.fromApiJsonHelper.parameterExists(IS_LINKED_TO_FLOATING_INTEREST_RATES, element)
&& this.fromApiJsonHelper.extractBooleanNamed(IS_LINKED_TO_FLOATING_INTEREST_RATES, element)) {
if (isEqualAmortization) {
@@ -519,8 +522,7 @@ public final class LoanProductDataValidator {
"isFloatingInterestRateCalculationAllowed param is not supported when isLinkedToFloatingInterestRates is not supplied or false");
}
final BigDecimal interestRatePerPeriod = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(INTEREST_RATE_PER_PERIOD,
element);
interestRatePerPeriod = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(INTEREST_RATE_PER_PERIOD, element);
baseDataValidator.reset().parameter(INTEREST_RATE_PER_PERIOD).value(interestRatePerPeriod).notNull().zeroOrPositiveAmount();
final String minInterestRatePerPeriodParameterName = MIN_INTEREST_RATE_PER_PERIOD;
@@ -564,6 +566,10 @@ public final class LoanProductDataValidator {
4);
}
// Fixed Length validation
fixedLengthValidations(transactionProcessingStrategyCode, interestRatePerPeriod, numberOfRepayments, repaymentEvery, element,
baseDataValidator);
// Guarantee Funds
Boolean holdGuaranteeFunds = false;
if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.holdGuaranteeFundsParamName, element)) {
@@ -1238,13 +1244,15 @@ public final class LoanProductDataValidator {
.integerGreaterThanZero();
}
Integer numberOfRepayments = loanProduct.getNumberOfRepayments();
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();
}
Integer repaymentEvery = loanProduct.getLoanProductRelatedDetail().getRepayEvery();
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();
}
@@ -1364,6 +1372,7 @@ public final class LoanProductDataValidator {
}
// interest rates
BigDecimal interestRatePerPeriod = null;
boolean isLinkedToFloatingInterestRates = loanProduct.isLinkedToFloatingInterestRate();
if (this.fromApiJsonHelper.parameterExists(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()
.zeroOrPositiveAmount();
BigDecimal interestRatePerPeriod = loanProduct.getLoanProductRelatedDetail().getNominalInterestRatePerPeriod();
interestRatePerPeriod = loanProduct.getLoanProductRelatedDetail().getNominalInterestRatePerPeriod();
if (this.fromApiJsonHelper.parameterExists(INTEREST_RATE_PER_PERIOD, element)) {
interestRatePerPeriod = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(INTEREST_RATE_PER_PERIOD, element);
}
@@ -1546,6 +1555,10 @@ public final class LoanProductDataValidator {
4);
}
// Fixed Length validation
fixedLengthValidations(transactionProcessingStrategyCode, interestRatePerPeriod, numberOfRepayments, repaymentEvery, element,
baseDataValidator);
// Guarantee Funds
Boolean holdGuaranteeFunds = loanProduct.isHoldGuaranteeFundsEnabled();
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);
}
}
}
}
}