FINERACT-2312: full term configuration added

This commit is contained in:
Attila Budai
2025-12-08 19:08:29 +01:00
committed by Adam Saghy
parent 035b90211c
commit a3fdfb920d
23 changed files with 492 additions and 100 deletions
@@ -195,6 +195,8 @@ public interface LoanApiConstants {
String INTEREST_RECOGNITION_ON_DISBURSEMENT_DATE = "interestRecognitionOnDisbursementDate";
String ALLOW_FULL_TERM_FOR_TRANCHE = "allowFullTermForTranche";
// Loan Summary Transaction Types
List<LoanTransactionType> LOAN_SUMMARY_TRANSACTION_TYPES = List.of(LoanTransactionType.CHARGE_ADJUSTMENT, //
LoanTransactionType.CHARGEBACK, //
@@ -269,6 +269,7 @@ public class LoanAccountData {
private Boolean enableInstallmentLevelDelinquency;
private LocalDate lastClosedBusinessDate;
private Boolean chargedOff;
private Boolean allowFullTermForTranche;
private Boolean enableDownPayment;
private BigDecimal disbursedAmountPercentageForDownPayment;
@@ -480,12 +481,12 @@ public class LoanAccountData {
final BigDecimal disbursedAmountPercentageForDownPayment, final boolean enableAutoRepaymentForDownPayment,
final boolean enableInstallmentLevelDelinquency, final EnumOptionData loanScheduleType,
final EnumOptionData loanScheduleProcessingType, final Integer fixedLength, final StringEnumOptionData chargeOffBehaviour,
final boolean isInterestRecognitionOnDisbursementDate, final StringEnumOptionData daysInYearCustomStrategy,
final boolean enableIncomeCapitalization, final StringEnumOptionData capitalizedIncomeCalculationType,
final StringEnumOptionData capitalizedIncomeStrategy, StringEnumOptionData capitalizedIncomeType,
final boolean enableBuyDownFee, final StringEnumOptionData buyDownFeeCalculationType,
final StringEnumOptionData buyDownFeeStrategy, final StringEnumOptionData buyDownFeeIncomeType,
final boolean merchantBuyDownFee) {
final boolean isInterestRecognitionOnDisbursementDate, final boolean allowFullTermForTranche,
final StringEnumOptionData daysInYearCustomStrategy, final boolean enableIncomeCapitalization,
final StringEnumOptionData capitalizedIncomeCalculationType, final StringEnumOptionData capitalizedIncomeStrategy,
StringEnumOptionData capitalizedIncomeType, final boolean enableBuyDownFee,
final StringEnumOptionData buyDownFeeCalculationType, final StringEnumOptionData buyDownFeeStrategy,
final StringEnumOptionData buyDownFeeIncomeType, final boolean merchantBuyDownFee) {
final CollectionData delinquent = CollectionData.template();
@@ -531,7 +532,8 @@ public class LoanAccountData {
.setEnableInstallmentLevelDelinquency(enableInstallmentLevelDelinquency).setLoanScheduleType(loanScheduleType)
.setLoanScheduleProcessingType(loanScheduleProcessingType).setFixedLength(fixedLength)
.setChargeOffBehaviour(chargeOffBehaviour).setInterestRecognitionOnDisbursementDate(isInterestRecognitionOnDisbursementDate)
.setDaysInYearCustomStrategy(daysInYearCustomStrategy).setEnableIncomeCapitalization(enableIncomeCapitalization)
.setAllowFullTermForTranche(allowFullTermForTranche).setDaysInYearCustomStrategy(daysInYearCustomStrategy)
.setEnableIncomeCapitalization(enableIncomeCapitalization)
.setCapitalizedIncomeCalculationType(capitalizedIncomeCalculationType)
.setCapitalizedIncomeStrategy(capitalizedIncomeStrategy).setCapitalizedIncomeType(capitalizedIncomeType)
.setEnableBuyDownFee(enableBuyDownFee).setBuyDownFeeCalculationType(buyDownFeeCalculationType)
@@ -427,6 +427,9 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@Column(name = "enable_installment_level_delinquency", nullable = false)
private boolean enableInstallmentLevelDelinquency = false;
@Column(name = "allow_full_term_for_tranche", nullable = false)
private boolean allowFullTermForTranche = false;
public static Loan newIndividualLoanApplication(final String accountNo, final Client client, final AccountType loanType,
final LoanProduct loanProduct, final Fund fund, final Staff officer, final CodeValue loanPurpose,
final LoanRepaymentScheduleTransactionProcessor transactionProcessingStrategy,
@@ -436,12 +439,12 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
final Boolean createStandingInstructionAtDisbursement, final Boolean isFloatingInterestRate,
final BigDecimal interestRateDifferential, final List<Rate> rates, final BigDecimal fixedPrincipalPercentagePerInstallment,
final ExternalId externalId, final LoanApplicationTerms loanApplicationTerms, final Boolean enableInstallmentLevelDelinquency,
final LocalDate submittedOnDate) {
final LocalDate submittedOnDate, final Boolean allowFullTermForTranche) {
return new Loan(accountNo, client, null, loanType, fund, officer, loanPurpose, transactionProcessingStrategy, loanProduct,
loanRepaymentScheduleDetail, null, loanCharges, collateral, null, fixedEmiAmount, disbursementDetails,
maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, isFloatingInterestRate, interestRateDifferential, rates,
fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms, enableInstallmentLevelDelinquency,
submittedOnDate);
submittedOnDate, allowFullTermForTranche);
}
public static Loan newGroupLoanApplication(final String accountNo, final Group group, final AccountType loanType,
@@ -453,12 +456,12 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
final Boolean createStandingInstructionAtDisbursement, final Boolean isFloatingInterestRate,
final BigDecimal interestRateDifferential, final List<Rate> rates, final BigDecimal fixedPrincipalPercentagePerInstallment,
final ExternalId externalId, final LoanApplicationTerms loanApplicationTerms, final Boolean enableInstallmentLevelDelinquency,
final LocalDate submittedOnDate) {
final LocalDate submittedOnDate, final Boolean allowFullTermForTranche) {
return new Loan(accountNo, null, group, loanType, fund, officer, loanPurpose, transactionProcessingStrategy, loanProduct,
loanRepaymentScheduleDetail, null, loanCharges, null, syncDisbursementWithMeeting, fixedEmiAmount, disbursementDetails,
maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, isFloatingInterestRate, interestRateDifferential, rates,
fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms, enableInstallmentLevelDelinquency,
submittedOnDate);
submittedOnDate, allowFullTermForTranche);
}
public static Loan newIndividualLoanApplicationFromGroup(final String accountNo, final Client client, final Group group,
@@ -470,12 +473,12 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
final Boolean createStandingInstructionAtDisbursement, final Boolean isFloatingInterestRate,
final BigDecimal interestRateDifferential, final List<Rate> rates, final BigDecimal fixedPrincipalPercentagePerInstallment,
final ExternalId externalId, final LoanApplicationTerms loanApplicationTerms, final Boolean enableInstallmentLevelDelinquency,
final LocalDate submittedOnDate) {
final LocalDate submittedOnDate, final Boolean allowFullTermForTranche) {
return new Loan(accountNo, client, group, loanType, fund, officer, loanPurpose, transactionProcessingStrategy, loanProduct,
loanRepaymentScheduleDetail, null, loanCharges, null, syncDisbursementWithMeeting, fixedEmiAmount, disbursementDetails,
maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, isFloatingInterestRate, interestRateDifferential, rates,
fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms, enableInstallmentLevelDelinquency,
submittedOnDate);
submittedOnDate, allowFullTermForTranche);
}
protected Loan() {
@@ -491,7 +494,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
final Boolean createStandingInstructionAtDisbursement, final Boolean isFloatingInterestRate,
final BigDecimal interestRateDifferential, final List<Rate> rates, final BigDecimal fixedPrincipalPercentagePerInstallment,
final ExternalId externalId, final LoanApplicationTerms loanApplicationTerms, final Boolean enableInstallmentLevelDelinquency,
final LocalDate submittedOnDate) {
final LocalDate submittedOnDate, final Boolean allowFullTermForTranche) {
this.loanRepaymentScheduleDetail = loanRepaymentScheduleDetail;
this.isFloatingInterestRate = isFloatingInterestRate;
@@ -572,6 +575,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
this.loanInterestRecalculationDetails.updateLoan(this);
}
this.enableInstallmentLevelDelinquency = enableInstallmentLevelDelinquency;
this.allowFullTermForTranche = allowFullTermForTranche;
this.getLoanProductRelatedDetail()
.setEnableAccrualActivityPosting(loanProduct.getLoanProductRelatedDetail().isEnableAccrualActivityPosting());
}
@@ -1783,6 +1787,14 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
this.enableInstallmentLevelDelinquency = enableInstallmentLevelDelinquency;
}
public boolean isAllowFullTermForTranche() {
return this.allowFullTermForTranche;
}
public void updateAllowFullTermForTranche(boolean allowFullTermForTranche) {
this.allowFullTermForTranche = allowFullTermForTranche;
}
public void deductFromNetDisbursalAmount(final BigDecimal subtrahend) {
this.netDisbursalAmount = this.netDisbursalAmount.subtract(subtrahend);
}
@@ -72,6 +72,7 @@ public interface LoanProductConstants {
String MULTI_DISBURSE_LOAN_PARAMETER_NAME = "multiDisburseLoan";
String MAX_TRANCHE_COUNT_PARAMETER_NAME = "maxTrancheCount";
String OUTSTANDING_LOAN_BALANCE_PARAMETER_NAME = "outstandingLoanBalance";
String ALLOW_FULL_TERM_FOR_TRANCHE_PARAM_NAME = "allowFullTermForTranche";
String GRACE_ON_ARREARS_AGEING_PARAMETER_NAME = "graceOnArrearsAgeing";
String OVERDUE_DAYS_FOR_NPA_PARAMETER_NAME = "overdueDaysForNPA";
@@ -309,6 +309,8 @@ public final class LoanProductsApiResourceSwagger {
// Multi Disburse
@Schema(example = "true")
public Boolean multiDisburseLoan;
@Schema(example = "false", description = "Allow full term length for each tranche disbursement. Only available for PROGRESSIVE schedule type with multi-disbursement enabled.")
public Boolean allowFullTermForTranche;
@Schema(example = "50")
public Integer principalThresholdForLastInstallment;
@Schema(example = "true")
@@ -1481,6 +1483,8 @@ public final class LoanProductsApiResourceSwagger {
public Boolean isRatesEnabled;
@Schema(example = "true")
public Boolean multiDisburseLoan;
@Schema(example = "false", description = "Allow full term length for each tranche disbursement. Only available for PROGRESSIVE schedule type with multi-disbursement enabled.")
public Boolean allowFullTermForTranche;
@Schema(example = "3")
public Integer maxTrancheCount;
@Schema(example = "36000.000000")
@@ -1794,6 +1798,8 @@ public final class LoanProductsApiResourceSwagger {
// Multi Disburse
@Schema(example = "true")
public Boolean multiDisburseLoan;
@Schema(example = "false", description = "Allow full term length for each tranche disbursement. Only available for PROGRESSIVE schedule type with multi-disbursement enabled.")
public Boolean allowFullTermForTranche;
@Schema(example = "50")
public Integer principalThresholdForLastInstallment;
@Schema(example = "true")
@@ -217,6 +217,7 @@ public class LoanProductData implements Serializable {
private final Boolean allowApprovedDisbursedAmountsOverApplied;
private final String overAppliedCalculationType;
private final Integer overAppliedNumber;
private final Boolean allowFullTermForTranche;
private final BigDecimal principalThresholdForLastInstallment;
@@ -332,6 +333,7 @@ public class LoanProductData implements Serializable {
final Boolean allowApprovedDisbursedAmountsOverApplied = false;
final String overAppliedCalculationType = null;
final Integer overAppliedNumber = null;
final Boolean allowFullTermForTranche = null;
final LoanProductGuaranteeData productGuaranteeData = null;
final boolean holdGuaranteeFunds = false;
@@ -392,21 +394,21 @@ public class LoanProductData implements Serializable {
includeInBorrowerCycle, useBorrowerCycle, startDate, closeDate, status, externalId, principalVariations,
interestRateVariations, numberOfRepaymentVariations, multiDisburseLoan, maxTrancheCount, outstandingLoanBalance,
disallowExpectedDisbursements, allowApprovedDisbursedAmountsOverApplied, overAppliedCalculationType, overAppliedNumber,
graceOnArrearsAgeing, overdueDaysForNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled,
interestRecalculationData, minimumDaysBetweenDisbursalAndFirstRepayment, holdGuaranteeFunds, productGuaranteeData,
principalThresholdForLastInstallment, accountMovesOutOfNPAOnlyOnArrearsCompletion, canDefineInstallmentAmount,
installmentAmountInMultiplesOf, loanProductConfigurableAttributes, isLinkedToFloatingInterestRates, floatingRateId,
floatingRateName, interestRateDifferential, minDifferentialLendingRate, defaultDifferentialLendingRate,
maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGap, maximumGap,
syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled,
fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent,
overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment,
paymentAllocation, creditAllocation, repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType,
loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour,
interestRecognitionOnDisbursementDate, daysInYearTypeCustomStrategy, enableIncomeCapitalization,
capitalizedIncomeCalculationType, capitalizedIncomeStrategy, capitalizedIncomeType, enableBuyDownFee,
buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType, merchantBuyDownFee, writeOffReasonsToExpenseMappings,
writeOffReasonOptions);
allowFullTermForTranche, graceOnArrearsAgeing, overdueDaysForNPA, daysInMonthType, daysInYearType,
isInterestRecalculationEnabled, interestRecalculationData, minimumDaysBetweenDisbursalAndFirstRepayment, holdGuaranteeFunds,
productGuaranteeData, principalThresholdForLastInstallment, accountMovesOutOfNPAOnlyOnArrearsCompletion,
canDefineInstallmentAmount, installmentAmountInMultiplesOf, loanProductConfigurableAttributes,
isLinkedToFloatingInterestRates, floatingRateId, floatingRateName, interestRateDifferential, minDifferentialLendingRate,
defaultDifferentialLendingRate, maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed,
isVariableInstallmentsAllowed, minimumGap, maximumGap, syncExpectedWithDisbursementDate, canUseForTopup,
isEqualAmortization, rateOptions, rates, isRatesEnabled, fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions,
delinquencyBucket, dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent, enableDownPayment,
disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment, paymentAllocation, creditAllocation,
repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType, loanScheduleProcessingType, fixedLength,
enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour, interestRecognitionOnDisbursementDate,
daysInYearTypeCustomStrategy, enableIncomeCapitalization, capitalizedIncomeCalculationType, capitalizedIncomeStrategy,
capitalizedIncomeType, enableBuyDownFee, buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType,
merchantBuyDownFee, writeOffReasonsToExpenseMappings, writeOffReasonOptions);
}
@@ -473,6 +475,7 @@ public class LoanProductData implements Serializable {
final Boolean allowApprovedDisbursedAmountsOverApplied = false;
final String overAppliedCalculationType = null;
final Integer overAppliedNumber = null;
final Boolean allowFullTermForTranche = null;
final EnumOptionData daysInMonthType = null;
final EnumOptionData daysInYearType = null;
@@ -532,21 +535,21 @@ public class LoanProductData implements Serializable {
includeInBorrowerCycle, useBorrowerCycle, startDate, closeDate, status, externalId, principalVariations,
interestRateVariations, numberOfRepaymentVariations, multiDisburseLoan, maxTrancheCount, outstandingLoanBalance,
disallowExpectedDisbursements, allowApprovedDisbursedAmountsOverApplied, overAppliedCalculationType, overAppliedNumber,
graceOnArrearsAgeing, overdueDaysForNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled,
interestRecalculationData, minimumDaysBetweenDisbursalAndFirstRepayment, holdGuaranteeFunds, productGuaranteeData,
principalThresholdForLastInstallment, accountMovesOutOfNPAOnlyOnArrearsCompletion, canDefineInstallmentAmount,
installmentAmountInMultiplesOf, loanProductConfigurableAttributes, isLinkedToFloatingInterestRates, floatingRateId,
floatingRateName, interestRateDifferential, minDifferentialLendingRate, defaultDifferentialLendingRate,
maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGap, maximumGap,
syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled,
fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent,
overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment,
paymentAllocation, creditAllocation, repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType,
loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour,
interestRecognitionOnDisbursementDate, daysInYearTypeCustomStrategy, enableIncomeCapitalization,
capitalizedIncomeCalculationType, capitalizedIncomeStrategy, capitalizedIncomeType, enableBuyDownFee,
buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType, merchantBuyDownFee, writeOffReasonsToExpenseMappings,
writeOffReasonOptions);
allowFullTermForTranche, graceOnArrearsAgeing, overdueDaysForNPA, daysInMonthType, daysInYearType,
isInterestRecalculationEnabled, interestRecalculationData, minimumDaysBetweenDisbursalAndFirstRepayment, holdGuaranteeFunds,
productGuaranteeData, principalThresholdForLastInstallment, accountMovesOutOfNPAOnlyOnArrearsCompletion,
canDefineInstallmentAmount, installmentAmountInMultiplesOf, loanProductConfigurableAttributes,
isLinkedToFloatingInterestRates, floatingRateId, floatingRateName, interestRateDifferential, minDifferentialLendingRate,
defaultDifferentialLendingRate, maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed,
isVariableInstallmentsAllowed, minimumGap, maximumGap, syncExpectedWithDisbursementDate, canUseForTopup,
isEqualAmortization, rateOptions, rates, isRatesEnabled, fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions,
delinquencyBucket, dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent, enableDownPayment,
disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment, paymentAllocation, creditAllocation,
repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType, loanScheduleProcessingType, fixedLength,
enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour, interestRecognitionOnDisbursementDate,
daysInYearTypeCustomStrategy, enableIncomeCapitalization, capitalizedIncomeCalculationType, capitalizedIncomeStrategy,
capitalizedIncomeType, enableBuyDownFee, buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType,
merchantBuyDownFee, writeOffReasonsToExpenseMappings, writeOffReasonOptions);
}
@@ -619,6 +622,7 @@ public class LoanProductData implements Serializable {
final Boolean allowApprovedDisbursedAmountsOverApplied = false;
final String overAppliedCalculationType = null;
final Integer overAppliedNumber = null;
final Boolean allowFullTermForTranche = null;
final EnumOptionData daysInMonthType = CommonEnumerations.daysInMonthType(DaysInMonthType.ACTUAL);
final EnumOptionData daysInYearType = CommonEnumerations.daysInYearType(DaysInYearType.ACTUAL);
@@ -679,21 +683,21 @@ public class LoanProductData implements Serializable {
includeInBorrowerCycle, useBorrowerCycle, startDate, closeDate, status, externalId, principalVariationsForBorrowerCycle,
interestRateVariationsForBorrowerCycle, numberOfRepaymentVariationsForBorrowerCycle, multiDisburseLoan, maxTrancheCount,
outstandingLoanBalance, disallowExpectedDisbursements, allowApprovedDisbursedAmountsOverApplied, overAppliedCalculationType,
overAppliedNumber, graceOnArrearsAgeing, overdueDaysForNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled,
interestRecalculationData, minimumDaysBetweenDisbursalAndFirstRepayment, holdGuaranteeFunds, productGuaranteeData,
principalThresholdForLastInstallment, accountMovesOutOfNPAOnlyOnArrearsCompletion, canDefineInstallmentAmount,
installmentAmountInMultiplesOf, loanProductConfigurableAttributes, isLinkedToFloatingInterestRates, floatingRateId,
floatingRateName, interestRateDifferential, minDifferentialLendingRate, defaultDifferentialLendingRate,
maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGap, maximumGap,
syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled,
fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent,
overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment,
paymentAllocation, creditAllocation, repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType,
loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour,
interestRecognitionOnDisbursementDate, daysInYearTypeCustomStrategy, enableIncomeCapitalization,
capitalizedIncomeCalculationType, capitalizedIncomeStrategy, capitalizedIncomeType, enableBuyDownFee,
buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType, merchantBuyDownFee, writeOffReasonsToExpenseMappings,
writeOffReasonOptions);
overAppliedNumber, allowFullTermForTranche, graceOnArrearsAgeing, overdueDaysForNPA, daysInMonthType, daysInYearType,
isInterestRecalculationEnabled, interestRecalculationData, minimumDaysBetweenDisbursalAndFirstRepayment, holdGuaranteeFunds,
productGuaranteeData, principalThresholdForLastInstallment, accountMovesOutOfNPAOnlyOnArrearsCompletion,
canDefineInstallmentAmount, installmentAmountInMultiplesOf, loanProductConfigurableAttributes,
isLinkedToFloatingInterestRates, floatingRateId, floatingRateName, interestRateDifferential, minDifferentialLendingRate,
defaultDifferentialLendingRate, maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed,
isVariableInstallmentsAllowed, minimumGap, maximumGap, syncExpectedWithDisbursementDate, canUseForTopup,
isEqualAmortization, rateOptions, rates, isRatesEnabled, fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions,
delinquencyBucket, dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent, enableDownPayment,
disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment, paymentAllocation, creditAllocation,
repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType, loanScheduleProcessingType, fixedLength,
enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour, interestRecognitionOnDisbursementDate,
daysInYearTypeCustomStrategy, enableIncomeCapitalization, capitalizedIncomeCalculationType, capitalizedIncomeStrategy,
capitalizedIncomeType, enableBuyDownFee, buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType,
merchantBuyDownFee, writeOffReasonsToExpenseMappings, writeOffReasonOptions);
}
@@ -760,6 +764,7 @@ public class LoanProductData implements Serializable {
final Boolean allowApprovedDisbursedAmountsOverApplied = false;
final String overAppliedCalculationType = null;
final Integer overAppliedNumber = null;
final Boolean allowFullTermForTranche = null;
final EnumOptionData daysInMonthType = CommonEnumerations.daysInMonthType(DaysInMonthType.ACTUAL);
final EnumOptionData daysInYearType = CommonEnumerations.daysInYearType(DaysInYearType.ACTUAL);
@@ -820,21 +825,21 @@ public class LoanProductData implements Serializable {
includeInBorrowerCycle, useBorrowerCycle, startDate, closeDate, status, externalId, principalVariationsForBorrowerCycle,
interestRateVariationsForBorrowerCycle, numberOfRepaymentVariationsForBorrowerCycle, multiDisburseLoan, maxTrancheCount,
outstandingLoanBalance, disallowExpectedDisbursements, allowApprovedDisbursedAmountsOverApplied, overAppliedCalculationType,
overAppliedNumber, graceOnArrearsAgeing, overdueDaysForNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled,
interestRecalculationData, minimumDaysBetweenDisbursalAndFirstRepayment, holdGuaranteeFunds, productGuaranteeData,
principalThresholdForLastInstallment, accountMovesOutOfNPAOnlyOnArrearsCompletion, canDefineInstallmentAmount,
installmentAmountInMultiplesOf, loanProductConfigurableAttributes, isLinkedToFloatingInterestRates, floatingRateId,
floatingRateName, interestRateDifferential, minDifferentialLendingRate, defaultDifferentialLendingRate,
maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGap, maximumGap,
syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled,
fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent,
overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment,
paymentAllocation, creditAllocationData, repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType,
loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour,
interestRecognitionOnDisbursementDate, daysInYearTypeCustomStrategy, enableIncomeCapitalization,
capitalizedIncomeCalculationType, capitalizedIncomeStrategy, capitalizedIncomeType, enableBuyDownFee,
buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType, merchantBuyDownFee, writeOffReasonsToExpenseMappings,
writeOffReasonOptions);
overAppliedNumber, allowFullTermForTranche, graceOnArrearsAgeing, overdueDaysForNPA, daysInMonthType, daysInYearType,
isInterestRecalculationEnabled, interestRecalculationData, minimumDaysBetweenDisbursalAndFirstRepayment, holdGuaranteeFunds,
productGuaranteeData, principalThresholdForLastInstallment, accountMovesOutOfNPAOnlyOnArrearsCompletion,
canDefineInstallmentAmount, installmentAmountInMultiplesOf, loanProductConfigurableAttributes,
isLinkedToFloatingInterestRates, floatingRateId, floatingRateName, interestRateDifferential, minDifferentialLendingRate,
defaultDifferentialLendingRate, maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed,
isVariableInstallmentsAllowed, minimumGap, maximumGap, syncExpectedWithDisbursementDate, canUseForTopup,
isEqualAmortization, rateOptions, rates, isRatesEnabled, fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions,
delinquencyBucket, dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent, enableDownPayment,
disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment, paymentAllocation, creditAllocationData,
repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType, loanScheduleProcessingType, fixedLength,
enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour, interestRecognitionOnDisbursementDate,
daysInYearTypeCustomStrategy, enableIncomeCapitalization, capitalizedIncomeCalculationType, capitalizedIncomeStrategy,
capitalizedIncomeType, enableBuyDownFee, buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType,
merchantBuyDownFee, writeOffReasonsToExpenseMappings, writeOffReasonOptions);
}
public static LoanProductData withAccountingDetails(final LoanProductData productData, final Map<String, Object> accountingMappings,
@@ -873,9 +878,9 @@ public class LoanProductData implements Serializable {
Collection<LoanProductBorrowerCycleVariationData> numberOfRepaymentVariations, Boolean multiDisburseLoan,
Integer maxTrancheCount, BigDecimal outstandingLoanBalance, final Boolean disallowExpectedDisbursements,
final Boolean allowApprovedDisbursedAmountsOverApplied, final String overAppliedCalculationType,
final Integer overAppliedNumber, final Integer graceOnArrearsAgeing, final Integer overdueDaysForNPA,
final EnumOptionData daysInMonthType, final EnumOptionData daysInYearType, final boolean isInterestRecalculationEnabled,
final LoanProductInterestRecalculationData interestRecalculationData,
final Integer overAppliedNumber, final Boolean allowFullTermForTranche, final Integer graceOnArrearsAgeing,
final Integer overdueDaysForNPA, final EnumOptionData daysInMonthType, final EnumOptionData daysInYearType,
final boolean isInterestRecalculationEnabled, final LoanProductInterestRecalculationData interestRecalculationData,
final Integer minimumDaysBetweenDisbursalAndFirstRepayment, boolean holdGuaranteeFunds,
final LoanProductGuaranteeData loanProductGuaranteeData, final BigDecimal principalThresholdForLastInstallment,
final boolean accountMovesOutOfNPAOnlyOnArrearsCompletion, boolean canDefineInstallmentAmount,
@@ -1000,6 +1005,7 @@ public class LoanProductData implements Serializable {
this.allowApprovedDisbursedAmountsOverApplied = allowApprovedDisbursedAmountsOverApplied;
this.overAppliedCalculationType = overAppliedCalculationType;
this.overAppliedNumber = overAppliedNumber;
this.allowFullTermForTranche = allowFullTermForTranche;
this.graceOnArrearsAgeing = graceOnArrearsAgeing;
this.overdueDaysForNPA = overdueDaysForNPA;
@@ -1189,6 +1195,7 @@ public class LoanProductData implements Serializable {
this.allowApprovedDisbursedAmountsOverApplied = productData.allowApprovedDisbursedAmountsOverApplied;
this.overAppliedCalculationType = productData.overAppliedCalculationType;
this.overAppliedNumber = productData.overAppliedNumber;
this.allowFullTermForTranche = productData.allowFullTermForTranche;
this.minimumDaysBetweenDisbursalAndFirstRepayment = productData.minimumDaysBetweenDisbursalAndFirstRepayment;
@@ -288,7 +288,7 @@ public class LoanProduct extends AbstractPersistableCustom<Long> {
final LoanCapitalizedIncomeStrategy capitalizedIncomeStrategy, final LoanCapitalizedIncomeType capitalizedIncomeType,
final boolean enableBuyDownFee, final LoanBuyDownFeeCalculationType buyDownFeeCalculationType,
final LoanBuyDownFeeStrategy buyDownFeeStrategy, final LoanBuyDownFeeIncomeType buyDownFeeIncomeType,
final boolean merchantBuyDownFee) {
final boolean merchantBuyDownFee, final boolean allowFullTermForTranche) {
this.fund = fund;
this.transactionProcessingStrategyCode = transactionProcessingStrategyCode;
@@ -364,7 +364,8 @@ public class LoanProduct extends AbstractPersistableCustom<Long> {
loanConfigurableAttributes.updateLoanProduct(this);
}
this.loanProductTrancheDetails = new LoanProductTrancheDetails(multiDisburseLoan, maxTrancheCount, outstandingLoanBalance);
this.loanProductTrancheDetails = new LoanProductTrancheDetails(multiDisburseLoan, maxTrancheCount, outstandingLoanBalance,
allowFullTermForTranche);
this.overdueDaysForNPA = overdueDaysForNPA;
this.productInterestRecalculationDetails = productInterestRecalculationDetails;
this.minimumDaysBetweenDisbursalAndFirstRepayment = minimumDaysBetweenDisbursalAndFirstRepayment;
@@ -763,4 +764,8 @@ public class LoanProduct extends AbstractPersistableCustom<Long> {
this.enableInstallmentLevelDelinquency = enableInstallmentLevelDelinquency;
}
public boolean isAllowFullTermForTranche() {
return this.loanProductTrancheDetails != null && this.loanProductTrancheDetails.isAllowFullTermForTranche();
}
}
@@ -38,15 +38,19 @@ public class LoanProductTrancheDetails {
@Column(name = "max_outstanding_loan_balance", scale = 6, precision = 19)
private BigDecimal outstandingLoanBalance;
@Column(name = "allow_full_term_for_tranche")
private boolean allowFullTermForTranche;
protected LoanProductTrancheDetails() {
// TODO Auto-generated constructor stub
}
public LoanProductTrancheDetails(final boolean multiDisburseLoan, final Integer maxTrancheCount,
final BigDecimal outstandingLoanBalance) {
final BigDecimal outstandingLoanBalance, final boolean allowFullTermForTranche) {
this.multiDisburseLoan = multiDisburseLoan;
this.maxTrancheCount = maxTrancheCount;
this.outstandingLoanBalance = outstandingLoanBalance;
this.allowFullTermForTranche = allowFullTermForTranche;
}
}
@@ -603,6 +603,8 @@ final class LoansApiResourceSwagger {
public String writeoffReason;
public GetLoansLoanIdLinkedAccount linkedAccount;
public Set<GetLoansLoanIdDisbursementDetails> disbursementDetails;
@Schema(example = "false", description = "Allow full term length for each tranche disbursement")
public Boolean allowFullTermForTranche;
@Schema(example = "1100.000000")
public BigDecimal fixedEmiAmount;
@Schema(example = "35000.000000")
@@ -1198,6 +1200,8 @@ final class LoansApiResourceSwagger {
public List<GetLoansLoanIdTransactions> transactions;
@Schema(description = "Set of GetLoansLoanIdDisbursementDetails")
public Set<GetLoansLoanIdDisbursementDetails> disbursementDetails;
@Schema(example = "false", description = "Allow full term length for each tranche disbursement")
public Boolean allowFullTermForTranche;
@Schema(description = "Delinquent data")
public GetLoansLoanIdDelinquencySummary delinquent;
@Schema(description = "Set of charges")
@@ -1344,6 +1348,8 @@ final class LoansApiResourceSwagger {
public String externalId;
@Schema(description = "List of PostLoansDisbursementData")
public List<PostLoansDisbursementData> disbursementData;
@Schema(example = "false", description = "Allow full term length for each tranche disbursement")
public Boolean allowFullTermForTranche;
@Schema(description = "Maximum allowed outstanding balance")
public BigDecimal maxOutstandingLoanBalance;
@Schema(example = "[2011, 10, 20]")
@@ -1550,6 +1556,8 @@ final class LoansApiResourceSwagger {
public List<PutLoansLoanIdChanges> charges;
public List<PutLoansLoanIdCollateral> collateral;
public List<PutLoansLoanIdDisbursementData> disbursementData;
@Schema(example = "false", description = "Allow full term length for each tranche disbursement")
public Boolean allowFullTermForTranche;
@Schema(example = "HORIZONTAL")
public String loanScheduleProcessingType;
@Schema(example = "false")
@@ -175,7 +175,8 @@ public final class LoanApplicationValidator {
LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE, LoanProductConstants.FIXED_LENGTH,
LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY, LoanProductConstants.ENABLE_DOWN_PAYMENT,
LoanProductConstants.ENABLE_AUTO_REPAYMENT_DOWN_PAYMENT, LoanProductConstants.DISBURSED_AMOUNT_PERCENTAGE_DOWN_PAYMENT,
LoanApiConstants.INTEREST_RECOGNITION_ON_DISBURSEMENT_DATE, LoanApiConstants.daysInYearCustomStrategyParameterName));
LoanApiConstants.INTEREST_RECOGNITION_ON_DISBURSEMENT_DATE, LoanApiConstants.daysInYearCustomStrategyParameterName,
LoanApiConstants.ALLOW_FULL_TERM_FOR_TRANCHE));
public static final String LOANAPPLICATION_UNDO = "loanapplication.undo";
private final FromJsonHelper fromApiJsonHelper;
@@ -324,6 +325,18 @@ public final class LoanApplicationValidator {
}
}
if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.ALLOW_FULL_TERM_FOR_TRANCHE, element)) {
final Boolean allowFullTermForTranche = this.fromApiJsonHelper
.extractBooleanNamed(LoanApiConstants.ALLOW_FULL_TERM_FOR_TRANCHE, element);
baseDataValidator.reset().parameter(LoanApiConstants.ALLOW_FULL_TERM_FOR_TRANCHE).value(allowFullTermForTranche)
.ignoreIfNull().validateForBooleanValue();
if (Boolean.TRUE.equals(allowFullTermForTranche) && !loanProduct.isAllowFullTermForTranche()) {
baseDataValidator.reset().parameter(LoanApiConstants.ALLOW_FULL_TERM_FOR_TRANCHE).failWithCode("not.allowed.by.product",
"Full term tranche cannot be enabled because the loan product does not allow it");
}
}
BigDecimal fixedPrincipalPercentagePerInstallment = this.fromApiJsonHelper
.extractBigDecimalWithLocaleNamed(LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName, element);
baseDataValidator.reset().parameter(LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName)
@@ -946,6 +959,18 @@ public final class LoanApplicationValidator {
}
}
if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.ALLOW_FULL_TERM_FOR_TRANCHE, element)) {
final Boolean allowFullTermForTranche = this.fromApiJsonHelper
.extractBooleanNamed(LoanApiConstants.ALLOW_FULL_TERM_FOR_TRANCHE, element);
baseDataValidator.reset().parameter(LoanApiConstants.ALLOW_FULL_TERM_FOR_TRANCHE).value(allowFullTermForTranche)
.ignoreIfNull().validateForBooleanValue();
if (Boolean.TRUE.equals(allowFullTermForTranche) && !loanProduct.isAllowFullTermForTranche()) {
baseDataValidator.reset().parameter(LoanApiConstants.ALLOW_FULL_TERM_FOR_TRANCHE).failWithCode("not.allowed.by.product",
"Full term tranche cannot be enabled because the loan product does not allow it");
}
}
BigDecimal fixedPrincipalPercentagePerInstallment = this.fromApiJsonHelper
.extractBigDecimalWithLocaleNamed(LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName, element);
baseDataValidator.reset().parameter(LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName)
@@ -248,6 +248,12 @@ public class LoanAssemblerImpl implements LoanAssembler {
isEnableInstallmentLevelDelinquency = loanProduct.isEnableInstallmentLevelDelinquency();
}
Boolean allowFullTermForTranche = this.fromApiJsonHelper
.extractBooleanNamed(LoanProductConstants.ALLOW_FULL_TERM_FOR_TRANCHE_PARAM_NAME, element);
if (allowFullTermForTranche == null) {
allowFullTermForTranche = loanProduct.isAllowFullTermForTranche();
}
final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
Long officeId = client != null ? client.getOffice().getId() : group.getOffice().getId();
final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(officeId,
@@ -262,19 +268,19 @@ public class LoanAssemblerImpl implements LoanAssembler {
syncDisbursementWithMeeting, fixedEmiAmount, disbursementDetails, maxOutstandingLoanBalance,
createStandingInstructionAtDisbursement, isFloatingInterestRate, interestRateDifferential, rates,
fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms, isEnableInstallmentLevelDelinquency,
submittedOnDate);
submittedOnDate, allowFullTermForTranche);
} else if (group != null) {
loanApplication = Loan.newGroupLoanApplication(accountNo, group, loanAccountType, loanProduct, fund, loanOfficer, loanPurpose,
transactionProcessingStrategy, loanProductRelatedDetail, loanCharges, syncDisbursementWithMeeting, fixedEmiAmount,
disbursementDetails, maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, isFloatingInterestRate,
interestRateDifferential, rates, fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms,
isEnableInstallmentLevelDelinquency, submittedOnDate);
isEnableInstallmentLevelDelinquency, submittedOnDate, allowFullTermForTranche);
} else if (client != null) {
loanApplication = Loan.newIndividualLoanApplication(accountNo, client, loanAccountType, loanProduct, fund, loanOfficer,
loanPurpose, transactionProcessingStrategy, loanProductRelatedDetail, loanCharges, collateral, fixedEmiAmount,
disbursementDetails, maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, isFloatingInterestRate,
interestRateDifferential, rates, fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms,
isEnableInstallmentLevelDelinquency, submittedOnDate);
isEnableInstallmentLevelDelinquency, submittedOnDate, allowFullTermForTranche);
} else {
throw new IllegalStateException("No loan application exists for either a client or group (or both).");
}
@@ -843,6 +849,14 @@ public class LoanAssemblerImpl implements LoanAssembler {
loan.updateEnableInstallmentLevelDelinquency(enableInstallmentLevelDelinquency);
}
// update allow full term for tranche
if (command.isChangeInBooleanParameterNamed(LoanProductConstants.ALLOW_FULL_TERM_FOR_TRANCHE_PARAM_NAME,
loan.isAllowFullTermForTranche())) {
final Boolean allowFullTermForTranche = command
.booleanObjectValueOfParameterNamed(LoanProductConstants.ALLOW_FULL_TERM_FOR_TRANCHE_PARAM_NAME);
loan.updateAllowFullTermForTranche(allowFullTermForTranche);
}
if (changes.containsKey("recalculateLoanSchedule")) {
changes.remove("recalculateLoanSchedule");
@@ -267,6 +267,9 @@ public class LoanProductAssembler {
final Integer overAppliedNumber = command.integerValueOfParameterNamed(LoanProductConstants.OVER_APPLIED_NUMBER);
final boolean allowFullTermForTranche = command.parameterExists(LoanProductConstants.ALLOW_FULL_TERM_FOR_TRANCHE_PARAM_NAME)
&& command.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.ALLOW_FULL_TERM_FOR_TRANCHE_PARAM_NAME);
final Integer dueDaysForRepaymentEvent = command.integerValueOfParameterNamed(LoanProductConstants.DUE_DAYS_FOR_REPAYMENT_EVENT);
final Integer overDueDaysForRepaymentEvent = command
.integerValueOfParameterNamed(LoanProductConstants.OVER_DUE_DAYS_FOR_REPAYMENT_EVENT);
@@ -353,7 +356,7 @@ public class LoanProductAssembler {
enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour, interestRecognitionOnDisbursementDate,
daysInYearCustomStrategy, enableIncomeCapitalization, capitalizedIncomeCalculationType, capitalizedIncomeStrategy,
capitalizedIncomeType, enableBuyDownFee, buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType,
merchantBuyDownFee);
merchantBuyDownFee, allowFullTermForTranche);
}
@@ -54,9 +54,18 @@ public class LoanProductTrancheDetailsUpdateUtil {
actualChanges.put(LoanProductConstants.OUTSTANDING_LOAN_BALANCE_PARAMETER_NAME, newValue);
loanProductTrancheDetails.setOutstandingLoanBalance(newValue);
}
if (command.isChangeInBooleanParameterNamed(LoanProductConstants.ALLOW_FULL_TERM_FOR_TRANCHE_PARAM_NAME,
loanProductTrancheDetails.isAllowFullTermForTranche())) {
final boolean newValue = command
.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.ALLOW_FULL_TERM_FOR_TRANCHE_PARAM_NAME);
actualChanges.put(LoanProductConstants.ALLOW_FULL_TERM_FOR_TRANCHE_PARAM_NAME, newValue);
loanProductTrancheDetails.setAllowFullTermForTranche(newValue);
}
} else {
loanProductTrancheDetails.setMaxTrancheCount(null);
loanProductTrancheDetails.setOutstandingLoanBalance(null);
loanProductTrancheDetails.setAllowFullTermForTranche(false);
}
}
}
@@ -865,7 +865,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
+ " topup.topup_amount as topupAmount, l.last_closed_business_date as lastClosedBusinessDate,l.overpaidon_date as overpaidOnDate, "
+ " l.is_charged_off as isChargedOff, l.charge_off_reason_cv_id as chargeOffReasonId, codec.code_value as chargeOffReason, l.charged_off_on_date as chargedOffOnDate, l.enable_down_payment as enableDownPayment, l.disbursed_amount_percentage_for_down_payment as disbursedAmountPercentageForDownPayment, l.enable_auto_repayment_for_down_payment as enableAutoRepaymentForDownPayment,"
+ " cobu.username as chargedOffByUsername, cobu.firstname as chargedOffByFirstname, cobu.lastname as chargedOffByLastname, l.loan_schedule_type as loanScheduleType, l.loan_schedule_processing_type as loanScheduleProcessingType, "
+ " l.charge_off_behaviour as chargeOffBehaviour, l.interest_recognition_on_disbursement_date as interestRecognitionOnDisbursementDate " //
+ " l.charge_off_behaviour as chargeOffBehaviour, l.interest_recognition_on_disbursement_date as interestRecognitionOnDisbursementDate, l.allow_full_term_for_tranche as allowFullTermForTranche " //
+ " from m_loan l" //
+ " join m_product_loan lp on lp.id = l.product_id" //
+ " left join m_loan_recalculation_details lir on lir.loan_id = l.id join m_currency rc on rc."
@@ -1238,6 +1238,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
final Integer fixedLength = JdbcSupport.getInteger(rs, "fixedLength");
final LoanChargeOffBehaviour chargeOffBehaviour = LoanChargeOffBehaviour.valueOf(rs.getString("chargeOffBehaviour"));
final boolean interestRecognitionOnDisbursementDate = rs.getBoolean("interestRecognitionOnDisbursementDate");
final boolean allowFullTermForTranche = rs.getBoolean("allowFullTermForTranche");
final StringEnumOptionData daysInYearCustomStrategy = ApiFacingEnum.getStringEnumOptionData(DaysInYearCustomStrategyType.class,
rs.getString("daysInYearCustomStrategy"));
final boolean enableIncomeCapitalization = rs.getBoolean("enableIncomeCapitalization");
@@ -1274,7 +1275,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
lastClosedBusinessDate, overpaidOnDate, isChargedOff, enableDownPayment, disbursedAmountPercentageForDownPayment,
enableAutoRepaymentForDownPayment, enableInstallmentLevelDelinquency, loanScheduleType.asEnumOptionData(),
loanScheduleProcessingType.asEnumOptionData(), fixedLength, chargeOffBehaviour.getValueAsStringEnumOptionData(),
interestRecognitionOnDisbursementDate, daysInYearCustomStrategy, enableIncomeCapitalization,
interestRecognitionOnDisbursementDate, allowFullTermForTranche, daysInYearCustomStrategy, enableIncomeCapitalization,
capitalizedIncomeCalculationType, capitalizedIncomeStrategy, capitalizedIncomeType, enableBuyDownFee,
buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType, merchantBuyDownFee);
}
@@ -208,7 +208,8 @@ public final class LoanProductDataValidator {
LoanProductAccountingParams.BUY_DOWN_EXPENSE.getValue(), LoanProductAccountingParams.INCOME_FROM_BUY_DOWN.getValue(),
LoanProductConstants.MERCHANT_BUY_DOWN_FEE_PARAM_NAME,
LoanProductAccountingParams.CAPITALIZED_INCOME_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS.getValue(), //
LoanProductAccountingParams.BUYDOWN_FEE_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS.getValue() //
LoanProductAccountingParams.BUYDOWN_FEE_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS.getValue(), //
LoanProductConstants.ALLOW_FULL_TERM_FOR_TRANCHE_PARAM_NAME //
));
private static final String[] SUPPORTED_LOAN_CONFIGURABLE_ATTRIBUTES = { LoanProductConstants.amortizationTypeParamName,
@@ -1062,6 +1063,43 @@ public final class LoanProductDataValidator {
baseDataValidator.reset().parameter(INTEREST_TYPE).value(interestType).ignoreIfNull().inMinMaxRange(0, 1);
}
// Determine effective values for allowFullTermForTranche validation
// For updates, fall back to existing product values if not in request
Boolean effectiveMultiDisburseLoan = multiDisburseLoan;
if (!this.fromApiJsonHelper.parameterExists(LoanProductConstants.MULTI_DISBURSE_LOAN_PARAMETER_NAME, element)
&& loanProduct != null) {
effectiveMultiDisburseLoan = loanProduct.isMultiDisburseLoan();
}
String effectiveLoanScheduleType = LoanScheduleType.CUMULATIVE.toString();
if (fromApiJsonHelper.parameterExists(LoanProductConstants.LOAN_SCHEDULE_TYPE, element)) {
effectiveLoanScheduleType = fromApiJsonHelper.extractStringNamed(LoanProductConstants.LOAN_SCHEDULE_TYPE, element);
} else if (loanProduct != null) {
effectiveLoanScheduleType = loanProduct.getLoanProductRelatedDetail().getLoanScheduleType().toString();
}
Boolean effectiveAllowFullTermForTranche = false;
if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.ALLOW_FULL_TERM_FOR_TRANCHE_PARAM_NAME, element)) {
effectiveAllowFullTermForTranche = this.fromApiJsonHelper
.extractBooleanNamed(LoanProductConstants.ALLOW_FULL_TERM_FOR_TRANCHE_PARAM_NAME, element);
baseDataValidator.reset().parameter(LoanProductConstants.ALLOW_FULL_TERM_FOR_TRANCHE_PARAM_NAME)
.value(effectiveAllowFullTermForTranche).ignoreIfNull().validateForBooleanValue();
} else if (loanProduct != null && loanProduct.getLoanProductTrancheDetails() != null) {
effectiveAllowFullTermForTranche = loanProduct.getLoanProductTrancheDetails().isAllowFullTermForTranche();
}
// Validate: allowFullTermForTranche requires multi-disburse and PROGRESSIVE schedule
if (Boolean.TRUE.equals(effectiveAllowFullTermForTranche)) {
if (!Boolean.TRUE.equals(effectiveMultiDisburseLoan)) {
baseDataValidator.reset().parameter(LoanProductConstants.ALLOW_FULL_TERM_FOR_TRANCHE_PARAM_NAME).failWithCode(
"requires.multi.disburse.loan", "Full term tranche can only be enabled for multi-disbursement loan products");
}
if (!LoanScheduleType.PROGRESSIVE.toString().equals(effectiveLoanScheduleType)) {
baseDataValidator.reset().parameter(LoanProductConstants.ALLOW_FULL_TERM_FOR_TRANCHE_PARAM_NAME).failWithCode(
"requires.progressive.schedule.type", "Full term tranche can only be enabled for PROGRESSIVE loan schedule type");
}
}
final String overAppliedCalculationType = this.fromApiJsonHelper.extractStringNamed(OVER_APPLIED_CALCULATION_TYPE, element);
baseDataValidator.reset().parameter(OVER_APPLIED_CALCULATION_TYPE).value(overAppliedCalculationType).notExceedingLengthOf(10);
}
@@ -254,7 +254,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
+ "lp.min_days_between_disbursal_and_first_repayment As minimumDaysBetweenDisbursalAndFirstRepayment, "
+ "lp.amortization_method_enum as amortizationMethod, lp.arrearstolerance_amount as tolerance, "
+ "lp.accounting_type as accountingType, lp.include_in_borrower_cycle as includeInBorrowerCycle,lp.use_borrower_cycle as useBorrowerCycle, lp.start_date as startDate, lp.close_date as closeDate, "
+ "lp.allow_multiple_disbursals as multiDisburseLoan, lp.max_disbursals as maxTrancheCount, lp.max_outstanding_loan_balance as outstandingLoanBalance, "
+ "lp.allow_multiple_disbursals as multiDisburseLoan, lp.max_disbursals as maxTrancheCount, lp.max_outstanding_loan_balance as outstandingLoanBalance, lp.allow_full_term_for_tranche as allowFullTermForTranche, "
+ "lp.disallow_expected_disbursements as disallowExpectedDisbursements, lp.allow_approved_disbursed_amounts_over_applied as allowApprovedDisbursedAmountsOverApplied, lp.over_applied_calculation_type as overAppliedCalculationType, over_applied_number as overAppliedNumber, "
+ "lp.days_in_month_enum as daysInMonth, lp.days_in_year_enum as daysInYear, lp.interest_recalculation_enabled as isInterestRecalculationEnabled, "
+ "lp.can_define_fixed_emi_amount as canDefineInstallmentAmount, lp.installment_amount_in_multiples_of as installmentAmountInMultiplesOf, "
@@ -439,6 +439,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
final Boolean allowApprovedDisbursedAmountsOverApplied = rs.getBoolean("allowApprovedDisbursedAmountsOverApplied");
final String overAppliedCalculationType = rs.getString("overAppliedCalculationType");
final Integer overAppliedNumber = rs.getInt("overAppliedNumber");
final Boolean allowFullTermForTranche = rs.getBoolean("allowFullTermForTranche");
final int daysInMonth = JdbcSupport.getInteger(rs, "daysInMonth");
final EnumOptionData daysInMonthType = CommonEnumerations.daysInMonthType(daysInMonth);
@@ -591,15 +592,16 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
principalVariationsForBorrowerCycle, interestRateVariationsForBorrowerCycle,
numberOfRepaymentVariationsForBorrowerCycle, multiDisburseLoan, maxTrancheCount, outstandingLoanBalance,
disallowExpectedDisbursements, allowApprovedDisbursedAmountsOverApplied, overAppliedCalculationType, overAppliedNumber,
graceOnArrearsAgeing, overdueDaysForNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled,
interestRecalculationData, minimumDaysBetweenDisbursalAndFirstRepayment, holdGuaranteeFunds, loanProductGuaranteeData,
principalThresholdForLastInstallment, accountMovesOutOfNPAOnlyOnArrearsCompletion, canDefineInstallmentAmount,
installmentAmountInMultiplesOf, allowAttributeOverrides, isLinkedToFloatingInterestRates, floatingRateId,
floatingRateName, interestRateDifferential, minDifferentialLendingRate, defaultDifferentialLendingRate,
maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableIntallmentsAllowed, minimumGap,
maximumGap, syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, this.rates,
isRatesEnabled, fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket,
dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageForDownPayment,
allowFullTermForTranche, graceOnArrearsAgeing, overdueDaysForNPA, daysInMonthType, daysInYearType,
isInterestRecalculationEnabled, interestRecalculationData, minimumDaysBetweenDisbursalAndFirstRepayment,
holdGuaranteeFunds, loanProductGuaranteeData, principalThresholdForLastInstallment,
accountMovesOutOfNPAOnlyOnArrearsCompletion, canDefineInstallmentAmount, installmentAmountInMultiplesOf,
allowAttributeOverrides, isLinkedToFloatingInterestRates, floatingRateId, floatingRateName, interestRateDifferential,
minDifferentialLendingRate, defaultDifferentialLendingRate, maxDifferentialLendingRate,
isFloatingInterestRateCalculationAllowed, isVariableIntallmentsAllowed, minimumGap, maximumGap,
syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, this.rates, isRatesEnabled,
fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent,
overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageForDownPayment,
enableAutoRepaymentForDownPayment, advancedPaymentData, creditAllocationData, repaymentStartDateType,
enableInstallmentLevelDelinquency, loanScheduleType.asEnumOptionData(), loanScheduleProcessingType.asEnumOptionData(),
fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes,
@@ -225,4 +225,5 @@
<include file="parts/0204_transaction_summary_with_asset_owner_and_from_asset_owner_id_for_buybacks.xml" relativeToChangelogFile="true" />
<include file="parts/0205_add_read_familymembers_permission.xml" relativeToChangelogFile="true" />
<include file="parts/0206_transaction_summary_with_asset_owner_classification_name_bug_fix.xml" relativeToChangelogFile="true" />
<include file="parts/0207_add_allow_full_term_for_tranche.xml" relativeToChangelogFile="true" />
</databaseChangeLog>
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet author="fineract" id="1">
<addColumn tableName="m_product_loan">
<column defaultValueBoolean="false" type="boolean" name="allow_full_term_for_tranche">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
<changeSet author="fineract" id="2">
<addColumn tableName="m_loan">
<column defaultValueBoolean="false" type="boolean" name="allow_full_term_for_tranche">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -80,6 +80,7 @@ public class LoanBuilder {
private ExternalId externalId = ExternalId.empty();
private LoanApplicationTerms loanApplicationTerms = mock(LoanApplicationTerms.class);
private Boolean enableInstallmentLevelDelinquency = false;
private Boolean allowFullTermForTranche = false;
private LocalDate submittedOnDate = LocalDate.now(ZoneId.systemDefault());
private LocalDate approvedOnDate;
private LocalDate expectedDisbursementDate;
@@ -115,7 +116,7 @@ public class LoanBuilder {
transactionProcessor, loanRepaymentScheduleDetail, charges, collateral, fixedEmiAmount, disbursementDetails,
maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, isFloatingInterestRate, interestRateDifferential,
rates, fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms, enableInstallmentLevelDelinquency,
submittedOnDate);
submittedOnDate, allowFullTermForTranche);
if (id != null) {
loan.setId(id);
@@ -338,6 +339,11 @@ public class LoanBuilder {
return this;
}
public LoanBuilder withAllowFullTermForTranche(Boolean allowFullTermForTranche) {
this.allowFullTermForTranche = allowFullTermForTranche;
return this;
}
public LoanBuilder withSubmittedOnDate(LocalDate submittedOnDate) {
this.submittedOnDate = submittedOnDate;
return this;
@@ -72,7 +72,7 @@ public class LoanProductValidationStepDefinitions implements En {
private LoanProduct newLoanProduct(boolean allowMultipleDisbursals) {
LoanProduct lp = new LoanProduct();
lp.setLoanProductTrancheDetails(new LoanProductTrancheDetails(allowMultipleDisbursals, null, null));
lp.setLoanProductTrancheDetails(new LoanProductTrancheDetails(allowMultipleDisbursals, null, null, false));
return lp;
}
}
@@ -33,15 +33,19 @@ import java.math.BigDecimal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicInteger;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.client.models.AdvancedPaymentData;
import org.apache.fineract.client.models.GetLoanProductsProductIdResponse;
import org.apache.fineract.client.models.GetLoansLoanIdDisbursementDetails;
import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod;
import org.apache.fineract.client.models.GetLoansLoanIdRepaymentSchedule;
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
import org.apache.fineract.client.models.PaymentAllocationOrder;
import org.apache.fineract.client.models.PostLoansLoanIdResponse;
import org.apache.fineract.client.models.PutLoansLoanIdResponse;
import org.apache.fineract.integrationtests.common.ClientHelper;
@@ -53,6 +57,8 @@ import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
import org.apache.fineract.integrationtests.common.loans.LoanStatusChecker;
import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension;
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -693,4 +699,185 @@ public class LoanDisbursementDetailsIntegrationTest {
equalTo("error.msg.loan.disbursal.amount.can't.be.greater.than.maximum.applied.loan.amount.calculation"))
.expectStatusCode(403).build();
}
private AdvancedPaymentData createDefaultPaymentAllocation(String futureInstallmentAllocationRule) {
AdvancedPaymentData advancedPaymentData = new AdvancedPaymentData();
advancedPaymentData.setTransactionType("DEFAULT");
advancedPaymentData.setFutureInstallmentAllocationRule(futureInstallmentAllocationRule);
List<PaymentAllocationOrder> paymentAllocationOrders = getPaymentAllocationOrder(PaymentAllocationType.PAST_DUE_PENALTY,
PaymentAllocationType.PAST_DUE_FEE, PaymentAllocationType.PAST_DUE_PRINCIPAL, PaymentAllocationType.PAST_DUE_INTEREST,
PaymentAllocationType.DUE_PENALTY, PaymentAllocationType.DUE_FEE, PaymentAllocationType.DUE_PRINCIPAL,
PaymentAllocationType.DUE_INTEREST, PaymentAllocationType.IN_ADVANCE_PENALTY, PaymentAllocationType.IN_ADVANCE_FEE,
PaymentAllocationType.IN_ADVANCE_PRINCIPAL, PaymentAllocationType.IN_ADVANCE_INTEREST);
advancedPaymentData.setPaymentAllocationOrder(paymentAllocationOrders);
return advancedPaymentData;
}
private List<PaymentAllocationOrder> getPaymentAllocationOrder(PaymentAllocationType... paymentAllocationTypes) {
AtomicInteger integer = new AtomicInteger(1);
return Arrays.stream(paymentAllocationTypes).map(pat -> {
PaymentAllocationOrder paymentAllocationOrder = new PaymentAllocationOrder();
paymentAllocationOrder.setPaymentAllocationRule(pat.name());
paymentAllocationOrder.setOrder(integer.getAndIncrement());
return paymentAllocationOrder;
}).toList();
}
@Test
public void testCreateLoanProductWithFullTermTrancheEnabled() {
AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation("NEXT_INSTALLMENT");
final String loanProductJSON = new LoanProductTestBuilder().withAmortizationTypeAsEqualInstallments()
.withInterestTypeAsDecliningBalance().withMoratorium("", "").withInterestCalculationPeriodTypeAsRepaymentPeriod(true)
.withInterestTypeAsDecliningBalance().withMultiDisburse().withLoanScheduleType(LoanScheduleType.PROGRESSIVE)
.addAdvancedPaymentAllocation(defaultAllocation).withAllowFullTermForTranche(true).build(null);
final Integer loanProductId = this.loanTransactionHelper.getLoanProductId(loanProductJSON);
log.info("------------------LOAN PRODUCT CREATED WITH ID----------- {}", loanProductId);
GetLoanProductsProductIdResponse loanProduct = this.loanTransactionHelper.getLoanProduct(loanProductId);
assertNotNull(loanProduct);
assertEquals(true, loanProduct.getMultiDisburseLoan());
assertEquals(true, loanProduct.getAllowFullTermForTranche());
log.info("-------------------LOAN PRODUCT WITH allowFullTermForTranche CREATED SUCCESSFULLY-------");
}
@Test
public void testCreateLoanProductWithFullTermTrancheOnCumulativeShouldFail() {
final ResponseSpecification errorResponse = new ResponseSpecBuilder()
.expectBody("userMessageGlobalisationCode", equalTo("validation.msg.validation.errors.exist"))
.expectBody("errors[0].userMessageGlobalisationCode",
equalTo("validation.msg.loanproduct.allowFullTermForTranche.requires.progressive.schedule.type"))
.expectStatusCode(400).build();
final LoanTransactionHelper validationErrorHelper = new LoanTransactionHelper(this.requestSpec, errorResponse);
final String loanProductJSON = new LoanProductTestBuilder().withAmortizationTypeAsEqualInstallments()
.withInterestTypeAsDecliningBalance().withMoratorium("", "").withInterestCalculationPeriodTypeAsRepaymentPeriod(true)
.withInterestTypeAsDecliningBalance().withMultiDisburse().withLoanScheduleType(LoanScheduleType.CUMULATIVE)
.withAllowFullTermForTranche(true).build(null);
validationErrorHelper.getLoanProductId(loanProductJSON);
log.info("-------------------LOAN PRODUCT WITH allowFullTermForTranche ON CUMULATIVE FAILED AS EXPECTED-------");
}
@Test
public void testCreateLoanProductWithFullTermTrancheOnSingleDisburseShouldFail() {
AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation("NEXT_INSTALLMENT");
final ResponseSpecification errorResponse = new ResponseSpecBuilder()
.expectBody("userMessageGlobalisationCode", equalTo("validation.msg.validation.errors.exist"))
.expectBody("errors[0].userMessageGlobalisationCode",
equalTo("validation.msg.loanproduct.allowFullTermForTranche.requires.multi.disburse.loan"))
.expectStatusCode(400).build();
final LoanTransactionHelper validationErrorHelper = new LoanTransactionHelper(this.requestSpec, errorResponse);
final String loanProductJSON = new LoanProductTestBuilder().withAmortizationTypeAsEqualInstallments()
.withInterestTypeAsDecliningBalance().withMoratorium("", "").withInterestCalculationPeriodTypeAsRepaymentPeriod(true)
.withInterestTypeAsDecliningBalance().withLoanScheduleType(LoanScheduleType.PROGRESSIVE)
.addAdvancedPaymentAllocation(defaultAllocation).withAllowFullTermForTranche(true).build(null);
validationErrorHelper.getLoanProductId(loanProductJSON);
log.info("-------------------LOAN PRODUCT WITH allowFullTermForTranche ON SINGLE DISBURSE FAILED AS EXPECTED-------");
}
@Test
public void testUpdateLoanProductPreservesAllowFullTermForTranche() {
AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation("NEXT_INSTALLMENT");
final String loanProductJSON = new LoanProductTestBuilder().withAmortizationTypeAsEqualInstallments()
.withInterestTypeAsDecliningBalance().withMoratorium("", "").withInterestCalculationPeriodTypeAsRepaymentPeriod(true)
.withInterestTypeAsDecliningBalance().withMultiDisburse().withLoanScheduleType(LoanScheduleType.PROGRESSIVE)
.addAdvancedPaymentAllocation(defaultAllocation).withAllowFullTermForTranche(true).build(null);
final Integer loanProductId = this.loanTransactionHelper.getLoanProductId(loanProductJSON);
log.info("------------------LOAN PRODUCT CREATED WITH ID----------- {}", loanProductId);
GetLoanProductsProductIdResponse loanProduct = this.loanTransactionHelper.getLoanProduct(loanProductId);
assertNotNull(loanProduct);
assertEquals(true, loanProduct.getAllowFullTermForTranche());
org.apache.fineract.client.models.PutLoanProductsProductIdRequest updateRequest = new org.apache.fineract.client.models.PutLoanProductsProductIdRequest();
updateRequest.setDescription("Updated description");
this.loanTransactionHelper.updateLoanProduct((long) loanProductId, updateRequest);
GetLoanProductsProductIdResponse updatedProduct = this.loanTransactionHelper.getLoanProduct(loanProductId);
assertNotNull(updatedProduct);
assertEquals(true, updatedProduct.getAllowFullTermForTranche());
assertEquals("Updated description", updatedProduct.getDescription());
log.info("-------------------LOAN PRODUCT UPDATE PRESERVED allowFullTermForTranche FLAG-------");
}
@Test
public void testLoanInheritsAllowFullTermForTrancheFromProduct() {
AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation("NEXT_INSTALLMENT");
final String loanProductJSON = new LoanProductTestBuilder().withAmortizationTypeAsEqualInstallments()
.withInterestTypeAsDecliningBalance().withMoratorium("", "").withInterestCalculationPeriodTypeAsRepaymentPeriod(true)
.withInterestTypeAsDecliningBalance().withMultiDisburse().withLoanScheduleType(LoanScheduleType.PROGRESSIVE)
.addAdvancedPaymentAllocation(defaultAllocation).withAllowFullTermForTranche(true).build(null);
final Integer loanProductId = this.loanTransactionHelper.getLoanProductId(loanProductJSON);
log.info("------------------LOAN PRODUCT CREATED WITH ID----------- {}", loanProductId);
final Integer clientId = ClientHelper.createClient(this.requestSpec, this.responseSpec);
log.info("------------------CLIENT CREATED WITH ID----------- {}", clientId);
List<HashMap> createTranches = new ArrayList<>();
createTranches.add(this.loanTransactionHelper.createTrancheDetail(null, "01 March 2014", "5000"));
createTranches.add(this.loanTransactionHelper.createTrancheDetail(null, "01 April 2014", "5000"));
final String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal("10000").withLoanTermFrequency("12")
.withLoanTermFrequencyAsMonths().withNumberOfRepayments("12").withRepaymentEveryAfter("1")
.withRepaymentFrequencyTypeAsMonths().withInterestRatePerPeriod("1").withExpectedDisbursementDate("01 March 2014")
.withTranches(createTranches).withSubmittedOnDate("01 March 2014")
.withRepaymentStrategy(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)
.build(clientId.toString(), loanProductId.toString(), null);
final Integer loanId = this.loanTransactionHelper.getLoanId(loanApplicationJSON);
log.info("------------------LOAN CREATED WITH ID----------- {}", loanId);
GetLoansLoanIdResponse loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanId);
assertNotNull(loanDetails);
assertEquals(true, loanDetails.getAllowFullTermForTranche());
log.info("-------------------LOAN INHERITED allowFullTermForTranche FROM PRODUCT SUCCESSFULLY-------");
}
@Test
public void testLoanLevelOverrideOfAllowFullTermForTranche() {
AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation("NEXT_INSTALLMENT");
final String loanProductJSON = new LoanProductTestBuilder().withAmortizationTypeAsEqualInstallments()
.withInterestTypeAsDecliningBalance().withMoratorium("", "").withInterestCalculationPeriodTypeAsRepaymentPeriod(true)
.withInterestTypeAsDecliningBalance().withMultiDisburse().withLoanScheduleType(LoanScheduleType.PROGRESSIVE)
.addAdvancedPaymentAllocation(defaultAllocation).withAllowFullTermForTranche(true).build(null);
final Integer loanProductId = this.loanTransactionHelper.getLoanProductId(loanProductJSON);
log.info("------------------LOAN PRODUCT CREATED WITH ID----------- {}", loanProductId);
final Integer clientId = ClientHelper.createClient(this.requestSpec, this.responseSpec);
log.info("------------------CLIENT CREATED WITH ID----------- {}", clientId);
List<HashMap> createTranches = new ArrayList<>();
createTranches.add(this.loanTransactionHelper.createTrancheDetail(null, "01 March 2014", "5000"));
createTranches.add(this.loanTransactionHelper.createTrancheDetail(null, "01 April 2014", "5000"));
final String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal("10000").withLoanTermFrequency("12")
.withLoanTermFrequencyAsMonths().withNumberOfRepayments("12").withRepaymentEveryAfter("1")
.withRepaymentFrequencyTypeAsMonths().withInterestRatePerPeriod("1").withExpectedDisbursementDate("01 March 2014")
.withTranches(createTranches).withSubmittedOnDate("01 March 2014")
.withRepaymentStrategy(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY).withAllowFullTermForTranche(false)
.build(clientId.toString(), loanProductId.toString(), null);
final Integer loanId = this.loanTransactionHelper.getLoanId(loanApplicationJSON);
log.info("------------------LOAN CREATED WITH ID----------- {}", loanId);
GetLoansLoanIdResponse loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanId);
assertNotNull(loanDetails);
assertEquals(false, loanDetails.getAllowFullTermForTranche());
log.info("-------------------LOAN LEVEL OVERRIDE OF allowFullTermForTranche WORKED SUCCESSFULLY-------");
}
}
@@ -88,6 +88,7 @@ public class LoanApplicationTestBuilder {
private boolean enableDownPayment = false;
private boolean enableAutoRepaymentForDownPayment = false;
private String disbursedAmountPercentageDownPayment;
private Boolean allowFullTermForTranche = null;
public String build(final String clientID, final String groupID, final String loanProductId, final String savingsID) {
final HashMap<String, Object> map = new HashMap<>();
@@ -220,6 +221,9 @@ public class LoanApplicationTestBuilder {
if (disbursedAmountPercentageDownPayment != null) {
map.put("disbursedAmountPercentageDownPayment", disbursedAmountPercentageDownPayment);
}
if (allowFullTermForTranche != null) {
map.put("allowFullTermForTranche", allowFullTermForTranche);
}
LOG.info("Loan Application request : {} ", map);
return new Gson().toJson(map);
}
@@ -468,4 +472,9 @@ public class LoanApplicationTestBuilder {
return this;
}
public LoanApplicationTestBuilder withAllowFullTermForTranche(final Boolean allowFullTermForTranche) {
this.allowFullTermForTranche = allowFullTermForTranche;
return this;
}
}
@@ -113,6 +113,7 @@ public class LoanProductTestBuilder {
private Account feeAndPenaltyAssetAccount;
private Boolean multiDisburseLoan = false;
private Boolean allowFullTermForTranche = false;
private final String outstandingLoanBalance = "35000";
private String maxTrancheCount = "3";
private Boolean disallowExpectedDisbursements = false;
@@ -225,6 +226,7 @@ public class LoanProductTestBuilder {
}
if (this.multiDisburseLoan) {
map.put("multiDisburseLoan", this.multiDisburseLoan);
map.put("allowFullTermForTranche", this.allowFullTermForTranche);
map.put("maxTrancheCount", this.maxTrancheCount);
map.put("outstandingLoanBalance", this.outstandingLoanBalance);
map.put("disallowExpectedDisbursements", this.disallowExpectedDisbursements);
@@ -242,6 +244,10 @@ public class LoanProductTestBuilder {
map.put("maxTrancheCount", this.maxTrancheCount);
map.put("outstandingLoanBalance", this.outstandingLoanBalance);
}
// Always send allowFullTermForTranche when it's true (for validation testing of single-disburse scenarios)
if (this.allowFullTermForTranche && !this.multiDisburseLoan) {
map.put("allowFullTermForTranche", this.allowFullTermForTranche);
}
if (this.fullAccountingConfig != null) {
map.putAll(this.fullAccountingConfig.toMap());
@@ -742,6 +748,11 @@ public class LoanProductTestBuilder {
return this;
}
public LoanProductTestBuilder withAllowFullTermForTranche(boolean allowFullTermForTranche) {
this.allowFullTermForTranche = allowFullTermForTranche;
return this;
}
public LoanProductTestBuilder withFeeToIncomeAccountMapping(final Long chargeId, final Long accountId) {
if (this.feeToIncomeAccountMappings == null) {
this.feeToIncomeAccountMappings = new ArrayList<>();