diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index 1810c7a7e..4a8e783d7 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -141,7 +141,6 @@ import org.apache.fineract.portfolio.loanproduct.domain.InterestRecalculationCom import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.apache.fineract.portfolio.loanproduct.domain.LoanRescheduleStrategyMethod; -import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType; import org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType; import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType; import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations; @@ -158,31 +157,10 @@ import org.apache.fineract.useradministration.domain.AppUser; public class Loan extends AbstractAuditableWithUTCDateTimeCustom { public static final String RECALCULATE_LOAN_SCHEDULE = "recalculateLoanSchedule"; - public static final String ACCOUNT_NO = "accountNo"; - public static final String IN_ARREARS_TOLERANCE = "inArrearsTolerance"; - public static final String CREATE_STANDING_INSTRUCTION_AT_DISBURSEMENT = "createStandingInstructionAtDisbursement"; public static final String EXTERNAL_ID = "externalId"; - public static final String CLIENT_ID = "clientId"; - public static final String GROUP_ID = "groupId"; - public static final String PRODUCT_ID = "productId"; - public static final String IS_FLOATING_INTEREST_RATE = "isFloatingInterestRate"; - public static final String INTEREST_RATE_DIFFERENTIAL = "interestRateDifferential"; - public static final String FUND_ID = "fundId"; - public static final String LOAN_OFFICER_ID = "loanOfficerId"; - public static final String LOAN_PURPOSE_ID = "loanPurposeId"; - public static final String TRANSACTION_PROCESSING_STRATEGY_CODE = "transactionProcessingStrategyCode"; - public static final String SUBMITTED_ON_DATE = "submittedOnDate"; public static final String DATE_FORMAT = "dateFormat"; public static final String LOCALE = "locale"; public static final String EXPECTED_DISBURSEMENT_DATE = "expectedDisbursementDate"; - public static final String REPAYMENTS_STARTING_FROM_DATE = "repaymentsStartingFromDate"; - public static final String SYNC_DISBURSEMENT_WITH_MEETING = "syncDisbursementWithMeeting"; - public static final String INTEREST_CHARGED_FROM_DATE = "interestChargedFromDate"; - public static final String PARAM_CHARGES = "charges"; - public static final String PARAM_COLLATERAL = "collateral"; - public static final String LOAN_TERM_FREQUENCY = "loanTermFrequency"; - public static final String LOAN_TERM_FREQUENCY_TYPE = "loanTermFrequencyType"; - public static final String PRINCIPAL = "principal"; public static final String PARAM_STATUS = "status"; public static final String REJECTED_ON_DATE = "rejectedOnDate"; public static final String CLOSED_ON_DATE = "closedOnDate"; @@ -198,7 +176,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { public static final String PENALTIES = "penalties"; public static final String EARLIEST_UNPAID_DATE = "earliest-unpaid-date"; public static final String NEXT_UNPAID_DUE_DATE = "next-unpaid-due-date"; - /** Disable optimistic locking till batch jobs failures can be fixed **/ + @Version int version; @@ -209,15 +187,15 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { private ExternalId externalId; @ManyToOne - @JoinColumn(name = "client_id", nullable = true) + @JoinColumn(name = "client_id") private Client client; @ManyToOne - @JoinColumn(name = "group_id", nullable = true) + @JoinColumn(name = "group_id") private Group group; @ManyToOne - @JoinColumn(name = "glim_id", nullable = true) + @JoinColumn(name = "glim_id") private GroupLoanIndividualMonitoringAccount glim; @Column(name = "loan_type_enum", nullable = false) @@ -228,15 +206,15 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { private LoanProduct loanProduct; @ManyToOne(optional = true, fetch = FetchType.EAGER) - @JoinColumn(name = "fund_id", nullable = true) + @JoinColumn(name = "fund_id") private Fund fund; @ManyToOne(fetch = FetchType.EAGER) - @JoinColumn(name = "loan_officer_id", nullable = true) + @JoinColumn(name = "loan_officer_id") private Staff loanOfficer; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "loanpurpose_cv_id", nullable = true) + @JoinColumn(name = "loanpurpose_cv_id") private CodeValue loanPurpose; @Column(name = "loan_transaction_strategy_code", nullable = false) @@ -267,7 +245,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { @Column(name = "loan_status_id", nullable = false) private Integer loanStatus; - @Column(name = "sync_disbursement_with_meeting", nullable = true) + @Column(name = "sync_disbursement_with_meeting") private Boolean syncDisbursementWithMeeting; // loan application states @@ -277,21 +255,21 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { private LocalDate rejectedOnDate; @ManyToOne(optional = true, fetch = FetchType.LAZY) - @JoinColumn(name = "rejectedon_userid", nullable = true) + @JoinColumn(name = "rejectedon_userid") private AppUser rejectedBy; @Column(name = "withdrawnon_date") private LocalDate withdrawnOnDate; @ManyToOne(optional = true, fetch = FetchType.LAZY) - @JoinColumn(name = "withdrawnon_userid", nullable = true) + @JoinColumn(name = "withdrawnon_userid") private AppUser withdrawnBy; @Column(name = "approvedon_date") private LocalDate approvedOnDate; @ManyToOne(optional = true, fetch = FetchType.LAZY) - @JoinColumn(name = "approvedon_userid", nullable = true) + @JoinColumn(name = "approvedon_userid") private AppUser approvedBy; @Column(name = "expected_disbursedon_date") @@ -301,14 +279,14 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { private LocalDate actualDisbursementDate; @ManyToOne(optional = true, fetch = FetchType.LAZY) - @JoinColumn(name = "disbursedon_userid", nullable = true) + @JoinColumn(name = "disbursedon_userid") private AppUser disbursedBy; @Column(name = "closedon_date") private LocalDate closedOnDate; @ManyToOne(optional = true, fetch = FetchType.LAZY) - @JoinColumn(name = "closedon_userid", nullable = true) + @JoinColumn(name = "closedon_userid") private AppUser closedBy; @Column(name = "writtenoffon_date") @@ -318,7 +296,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { private LocalDate rescheduledOnDate; @ManyToOne(optional = true, fetch = FetchType.LAZY) - @JoinColumn(name = "rescheduledon_userid", nullable = true) + @JoinColumn(name = "rescheduledon_userid") private AppUser rescheduledByUser; @Column(name = "expected_maturedon_date") @@ -372,7 +350,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { private LoanSummary summary; @Transient - private boolean accountNumberRequiresAutoGeneration = false; + private boolean accountNumberRequiresAutoGeneration; @Transient private LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory; @@ -390,10 +368,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { @Column(name = "net_disbursal_amount", scale = 6, precision = 19, nullable = false) private BigDecimal netDisbursalAmount; - @Column(name = "fixed_emi_amount", scale = 6, precision = 19, nullable = true) + @Column(name = "fixed_emi_amount", scale = 6, precision = 19) private BigDecimal fixedEmiAmount; - @Column(name = "max_outstanding_loan_balance", scale = 6, precision = 19, nullable = true) + @Column(name = "max_outstanding_loan_balance", scale = 6, precision = 19) private BigDecimal maxOutstandingLoanBalance; @OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true, fetch = FetchType.LAZY) @@ -419,26 +397,26 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { @Column(name = "accrued_till") private LocalDate accruedTill; - @Column(name = "create_standing_instruction_at_disbursement", nullable = true) + @Column(name = "create_standing_instruction_at_disbursement") private Boolean createStandingInstructionAtDisbursement; - @Column(name = "guarantee_amount_derived", scale = 6, precision = 19, nullable = true) + @Column(name = "guarantee_amount_derived", scale = 6, precision = 19) private BigDecimal guaranteeAmountDerived; @Column(name = "interest_recalcualated_on") private LocalDate interestRecalculatedOn; - @Column(name = "is_floating_interest_rate", nullable = true) + @Column(name = "is_floating_interest_rate") private Boolean isFloatingInterestRate; - @Column(name = "interest_rate_differential", scale = 6, precision = 19, nullable = true) + @Column(name = "interest_rate_differential", scale = 6, precision = 19) private BigDecimal interestRateDifferential; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "writeoff_reason_cv_id", nullable = true) + @JoinColumn(name = "writeoff_reason_cv_id") private CodeValue writeOffReason; - @Column(name = "loan_sub_status_id", nullable = true) + @Column(name = "loan_sub_status_id") private Integer loanSubStatus; @Column(name = "is_topup", nullable = false) @@ -454,7 +432,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { @JoinTable(name = "m_loan_rate", joinColumns = @JoinColumn(name = "loan_id"), inverseJoinColumns = @JoinColumn(name = "rate_id")) private List rates; - @Column(name = "fixed_principal_percentage_per_installment", scale = 2, precision = 5, nullable = true) + @Column(name = "fixed_principal_percentage_per_installment", scale = 2, precision = 5) private BigDecimal fixedPrincipalPercentagePerInstallment; @Column(name = "last_closed_business_date") @@ -529,7 +507,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } protected Loan() { - this.client = null; + // empty } private Loan(final String accountNo, final Client client, final Group group, final AccountType loanType, final Fund fund, @@ -542,7 +520,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { final BigDecimal interestRateDifferential, final List rates, final BigDecimal fixedPrincipalPercentagePerInstallment, final ExternalId externalId, final LoanApplicationTerms loanApplicationTerms, final LoanScheduleModel loanScheduleModel, final Boolean enableInstallmentLevelDelinquency, final LocalDate submittedOnDate) { - this.loanRepaymentScheduleDetail = loanRepaymentScheduleDetail; this.isFloatingInterestRate = isFloatingInterestRate; @@ -658,16 +635,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } private BigDecimal deriveSumTotalOfChargesDueAtDisbursement() { - - Money chargesDue = Money.of(getCurrency(), BigDecimal.ZERO); - - for (final LoanCharge charge : getActiveCharges()) { - if (charge.isDueAtDisbursement()) { - chargesDue = chargesDue.plus(charge.amount()); - } - } - - return chargesDue.getAmount(); + return getActiveCharges().stream() // + .filter(LoanCharge::isDueAtDisbursement) // + .map(LoanCharge::amount) // + .reduce(BigDecimal.ZERO, BigDecimal::add); } private Set associateChargesWithThisLoan(final Set loanCharges) { @@ -687,12 +658,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return collateral; } - public boolean isAccountNumberRequiresAutoGeneration() { - return this.accountNumberRequiresAutoGeneration; - } - public void addLoanCharge(final LoanCharge loanCharge) { - if (isChargesAdditionAllowed() && loanCharge.isDueAtDisbursement()) { // Note: added this constraint to restrict adding disbursement // charges to a loan @@ -730,13 +696,11 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { loanCharge.update(chargeAmt, loanCharge.getDueLocalDate(), amount, fetchNumberOfInstallmensAfterExceptions(), totalChargeAmt); // NOTE: must add new loan charge to set of loan charges before - // reporcessing the repayment schedule. + // reprocessing the repayment schedule. if (this.charges == null) { this.charges = new HashSet<>(); } - this.charges.add(loanCharge); - this.summary = updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement()); // store Id's of existing loan transactions and existing reversed loan @@ -749,7 +713,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } public ChangedTransactionDetail reprocessTransactions() { - ChangedTransactionDetail changedTransactionDetail = null; + ChangedTransactionDetail changedTransactionDetail; final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory .determineProcessor(this.transactionProcessingStrategyCode); final List allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing(); @@ -770,10 +734,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { * * If "suppliedTransactionDate" is not passed Id, the transaction date is set to the loans due date if the due date * is lesser than todays date. If not, the transaction date is set to todays date - * - * @param loanCharge - * @param suppliedTransactionDate - * @return */ public LoanTransaction handleChargeAppliedTransaction(final LoanCharge loanCharge, final LocalDate suppliedTransactionDate) { final Money chargeAmount = loanCharge.getAmount(getCurrency()); @@ -784,8 +744,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { feeCharges = Money.zero(loanCurrency()); } - LocalDate transactionDate = null; - + LocalDate transactionDate; if (suppliedTransactionDate != null) { transactionDate = suppliedTransactionDate; } else { @@ -887,7 +846,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } public void removeLoanCharge(final LoanCharge loanCharge) { - validateLoanIsNotClosed(loanCharge); // NOTE: to remove this constraint requires that loan transactions @@ -908,13 +866,13 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory .determineProcessor(this.transactionProcessingStrategyCode); if (!loanCharge.isDueAtDisbursement() && loanCharge.isPaidOrPartiallyPaid(loanCurrency())) { - /**** + /* * TODO Vishwas Currently we do not allow removing a loan charge after a loan is approved (hence there is no * need to adjust any loan transactions). * * Consider removing this block of code or logically completing it for the future by getting the list of * affected Transactions - ***/ + */ final List allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing(); loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges()); @@ -930,7 +888,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { for (final LoanTransaction transaction : transactions) { if (transaction.isRepaymentAtDisbursement() && doesLoanChargePaidByContainLoanCharge(transaction.getLoanChargesPaid(), loanCharge)) { - final MonetaryCurrency currency = loanCurrency(); final Money chargeAmount = Money.of(currency, loanCharge.amount()); if (transaction.isGreaterThan(chargeAmount)) { @@ -953,19 +910,15 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } private boolean doesLoanChargePaidByContainLoanCharge(Set loanChargePaidBys, LoanCharge loanCharge) { - for (LoanChargePaidBy loanChargePaidBy : loanChargePaidBys) { - if (loanChargePaidBy.getLoanCharge().equals(loanCharge)) { - return true; - } - } - return false; + return loanChargePaidBys.stream() // + .anyMatch(loanChargePaidBy -> loanChargePaidBy.getLoanCharge().equals(loanCharge)); } public Map updateLoanCharge(final LoanCharge loanCharge, final JsonCommand command) { + validateLoanIsNotClosed(loanCharge); final Map actualChanges = new LinkedHashMap<>(3); - validateLoanIsNotClosed(loanCharge); if (getActiveCharges().contains(loanCharge)) { final BigDecimal amount = calculateAmountPercentageAppliedTo(loanCharge); final Map loanChargeChanges = loanCharge.update(command, amount); @@ -976,13 +929,13 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory .determineProcessor(this.transactionProcessingStrategyCode); if (!loanCharge.isDueAtDisbursement()) { - /**** + /* * TODO Vishwas Currently we do not allow waiving updating loan charge after a loan is approved (hence there * is no need to adjust any loan transactions). * * Consider removing this block of code or logically completing it for the future by getting the list of * affected Transactions - ***/ + */ final List allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing(); loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges()); @@ -997,41 +950,31 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return actualChanges; } - /** - * @param loanCharge - * @return - */ private BigDecimal calculateAmountPercentageAppliedTo(final LoanCharge loanCharge) { - BigDecimal amount = BigDecimal.ZERO; if (loanCharge.isOverdueInstallmentCharge()) { return loanCharge.getAmountPercentageAppliedTo(); } - switch (loanCharge.getChargeCalculation()) { - case PERCENT_OF_AMOUNT: - amount = getDerivedAmountForCharge(loanCharge); - break; - case PERCENT_OF_AMOUNT_AND_INTEREST: + + return switch (loanCharge.getChargeCalculation()) { + case PERCENT_OF_AMOUNT -> getDerivedAmountForCharge(loanCharge); + case PERCENT_OF_AMOUNT_AND_INTEREST -> { final BigDecimal totalInterestCharged = getTotalInterest(); if (isMultiDisburmentLoan() && loanCharge.isDisbursementCharge()) { - amount = getTotalAllTrancheDisbursementAmount().getAmount().add(totalInterestCharged); + yield getTotalAllTrancheDisbursementAmount().getAmount().add(totalInterestCharged); } else { - amount = getPrincipal().getAmount().add(totalInterestCharged); + yield getPrincipal().getAmount().add(totalInterestCharged); } - break; - case PERCENT_OF_INTEREST: - amount = getTotalInterest(); - break; - case PERCENT_OF_DISBURSEMENT_AMOUNT: + } + case PERCENT_OF_INTEREST -> getTotalInterest(); + case PERCENT_OF_DISBURSEMENT_AMOUNT -> { if (loanCharge.getTrancheDisbursementCharge() != null) { - amount = loanCharge.getTrancheDisbursementCharge().getloanDisbursementDetails().principal(); + yield loanCharge.getTrancheDisbursementCharge().getloanDisbursementDetails().principal(); } else { - amount = getPrincipal().getAmount(); + yield getPrincipal().getAmount(); } - break; - default: - break; - } - return amount; + } + case INVALID, FLAT -> BigDecimal.ZERO; + }; } private Money getTotalAllTrancheDisbursementAmount() { @@ -1044,9 +987,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return amount; } - /** - * @return - */ public BigDecimal getTotalInterest() { return this.loanSummaryWrapper.calculateTotalInterestCharged(getRepaymentScheduleInstallments(), getCurrency()).getAmount(); } @@ -1076,34 +1016,25 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { */ private Money calculateInstallmentChargeAmount(final ChargeCalculationType calculationType, final BigDecimal percentage, final LoanRepaymentScheduleInstallment installment) { - Money amount = Money.zero(getCurrency()); - Money percentOf = Money.zero(getCurrency()); - switch (calculationType) { - case PERCENT_OF_AMOUNT: - percentOf = installment.getPrincipal(getCurrency()); - break; - case PERCENT_OF_AMOUNT_AND_INTEREST: - percentOf = installment.getPrincipal(getCurrency()).plus(installment.getInterestCharged(getCurrency())); - break; - case PERCENT_OF_INTEREST: - percentOf = installment.getInterestCharged(getCurrency()); - break; - default: - break; - } - amount = amount.plus(LoanCharge.percentageOf(percentOf.getAmount(), percentage)); - return amount; + Money percentOf = switch (calculationType) { + case PERCENT_OF_AMOUNT -> installment.getPrincipal(getCurrency()); + case PERCENT_OF_AMOUNT_AND_INTEREST -> + installment.getPrincipal(getCurrency()).plus(installment.getInterestCharged(getCurrency())); + case PERCENT_OF_INTEREST -> installment.getInterestCharged(getCurrency()); + case PERCENT_OF_DISBURSEMENT_AMOUNT, INVALID, FLAT -> Money.zero(getCurrency()); + + }; + return Money.zero(getCurrency()) // + .plus(LoanCharge.percentageOf(percentOf.getAmount(), percentage)); } public LoanTransaction waiveLoanCharge(final LoanCharge loanCharge, final LoanLifecycleStateMachine loanLifecycleStateMachine, final Map changes, final List existingTransactionIds, final List existingReversedTransactionIds, final Integer loanInstallmentNumber, final ScheduleGeneratorDTO scheduleGeneratorDTO, final Money accruedCharge, final ExternalId externalId) { - validateLoanIsNotClosed(loanCharge); final Money amountWaived = loanCharge.waive(loanCurrency(), loanInstallmentNumber); - changes.put("amount", amountWaived.getAmount()); Money unrecognizedIncome = amountWaived.zero(); @@ -1197,14 +1128,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return this.client; } - public GroupLoanIndividualMonitoringAccount getGlim() { - return glim; - } - - public void setGlim(GroupLoanIndividualMonitoringAccount glim) { - this.glim = glim; - } - public LoanProduct loanProduct() { return this.loanProduct; } @@ -1254,7 +1177,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { public void updateLoanCharges(final Set loanCharges) { List existingCharges = fetchAllLoanChargeIds(); - /** Process new and updated charges **/ + /* Process new and updated charges **/ for (final LoanCharge loanCharge : loanCharges) { LoanCharge charge = loanCharge; // add new charges @@ -1296,7 +1219,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } - /** Updated deleted charges **/ + /* Updated deleted charges **/ for (Long id : existingCharges) { fetchLoanChargesById(id).setActive(false); } @@ -1337,7 +1260,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { updateLoanScheduleDependentDerivedFields(); updateLoanSummaryDerivedFields(); - applyAccurals(); + applyAccruals(); } public void updateLoanSchedule(final Collection installments) { @@ -1356,7 +1279,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } updateLoanScheduleDependentDerivedFields(); updateLoanSummaryDerivedFields(); - applyAccurals(); + applyAccruals(); } @@ -1373,7 +1296,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { /** * method updates accrual derived fields on installments and reverse the unprocessed transactions */ - private void applyAccurals() { + private void applyAccruals() { Collection accruals = retrieveListOfAccrualTransactions(); if (!accruals.isEmpty()) { if (isPeriodicAccrualAccountingEnabledOnLoanProduct()) { @@ -1391,28 +1314,28 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { for (LoanRepaymentScheduleInstallment installment : installments) { Money interest = Money.zero(getCurrency()); Money fee = Money.zero(getCurrency()); - Money penality = Money.zero(getCurrency()); + Money penalty = Money.zero(getCurrency()); for (LoanTransaction loanTransaction : accruals) { LocalDate transactionDateForRange = getDateForRangeCalculation(loanTransaction, isBasedOnSubmittedOnDate); boolean isInPeriod = LoanRepaymentScheduleProcessingWrapper.isInPeriod(transactionDateForRange, installment, installments); if (isInPeriod) { interest = interest.plus(loanTransaction.getInterestPortion(getCurrency())); fee = fee.plus(loanTransaction.getFeeChargesPortion(getCurrency())); - penality = penality.plus(loanTransaction.getPenaltyChargesPortion(getCurrency())); + penalty = penalty.plus(loanTransaction.getPenaltyChargesPortion(getCurrency())); if (installment.getFeeChargesCharged(getCurrency()).isLessThan(fee) || installment.getInterestCharged(getCurrency()).isLessThan(interest) - || installment.getPenaltyChargesCharged(getCurrency()).isLessThan(penality) + || installment.getPenaltyChargesCharged(getCurrency()).isLessThan(penalty) || (isInterestBearing() && DateUtils.isEqual(getAccruedTill(), loanTransaction.getTransactionDate()) && !DateUtils.isEqual(getAccruedTill(), installment.getDueDate()))) { interest = interest.minus(loanTransaction.getInterestPortion(getCurrency())); fee = fee.minus(loanTransaction.getFeeChargesPortion(getCurrency())); - penality = penality.minus(loanTransaction.getPenaltyChargesPortion(getCurrency())); + penalty = penalty.minus(loanTransaction.getPenaltyChargesPortion(getCurrency())); loanTransaction.reverse(); } } } - installment.updateAccrualPortion(interest, fee, penality); + installment.updateAccrualPortion(interest, fee, penalty); } LoanRepaymentScheduleInstallment lastInstallment = getLastLoanRepaymentScheduleInstallment(); for (LoanTransaction loanTransaction : accruals) { @@ -1453,11 +1376,9 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { loanTransaction.reverse(); handleChargeAppliedTransaction(loanCharge, loanTransaction.getTransactionDate()); } - } } } - } public void updateLoanScheduleDependentDerivedFields() { @@ -1468,7 +1389,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } private void updateLoanSummaryDerivedFields() { - if (isNotDisbursed()) { this.summary.zeroFields(); this.totalOverpaid = null; @@ -1487,7 +1407,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { this.charges); updateLoanOutstandingBalances(); } - } public void updateLoanSummaryAndStatus() { @@ -1515,7 +1434,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { /** * Update interest recalculation settings if product configuration changes */ - private void updateOverdueScheduleInstallment(final LoanCharge loanCharge) { if (loanCharge.isOverdueInstallmentCharge() && loanCharge.isActive()) { LoanOverdueInstallmentCharge overdueInstallmentCharge = loanCharge.getOverdueInstallmentCharge(); @@ -1585,6 +1503,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { LoanRepaymentScheduleInstallment installment = loanCharge.getOverdueInstallmentCharge().getInstallment(); LocalDate graceDate = DateUtils.getBusinessLocalDate().minusDays(penaltyWaitPeriod); Money amount = Money.zero(getCurrency()); + if (DateUtils.isAfter(graceDate, installment.getDueDate())) { amount = calculateOverdueAmountPercentageAppliedTo(installment, loanCharge.getChargeCalculation()); if (!amount.isGreaterThanZero()) { @@ -1598,21 +1517,13 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { private Money calculateOverdueAmountPercentageAppliedTo(LoanRepaymentScheduleInstallment installment, ChargeCalculationType calculationType) { - Money amount = Money.zero(getCurrency()); - switch (calculationType) { - case PERCENT_OF_AMOUNT: - amount = installment.getPrincipalOutstanding(getCurrency()); - break; - case PERCENT_OF_AMOUNT_AND_INTEREST: - amount = installment.getPrincipalOutstanding(getCurrency()).plus(installment.getInterestOutstanding(getCurrency())); - break; - case PERCENT_OF_INTEREST: - amount = installment.getInterestOutstanding(getCurrency()); - break; - default: - break; - } - return amount; + return switch (calculationType) { + case PERCENT_OF_AMOUNT -> installment.getPrincipalOutstanding(getCurrency()); + case PERCENT_OF_AMOUNT_AND_INTEREST -> + installment.getPrincipalOutstanding(getCurrency()).plus(installment.getInterestOutstanding(getCurrency())); + case PERCENT_OF_INTEREST -> installment.getInterestOutstanding(getCurrency()); + default -> Money.zero(getCurrency()); + }; } // This method returns date format and locale if present in the JsonCommand @@ -1674,28 +1585,21 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } public void updateDisbursementDetails(final JsonCommand jsonCommand, final Map actualChanges) { - List disbursementList = fetchDisbursementIds(); List loanChargeIds = fetchLoanTrancheChargeIds(); int chargeIdLength = loanChargeIds.size(); - String chargeIds = null; + String chargeIds; // From modify application page, if user removes all charges, we should // get empty array. // So we need to remove all charges applied for this loan - boolean removeAllChages = false; - if (jsonCommand.parameterExists(LoanApiConstants.chargesParameterName)) { - JsonArray chargesArray = jsonCommand.arrayOfParameterNamed(LoanApiConstants.chargesParameterName); - if (chargesArray.size() == 0) { - removeAllChages = true; - } - } + boolean removeAllCharges = jsonCommand.parameterExists(LoanApiConstants.chargesParameterName) + && jsonCommand.arrayOfParameterNamed(LoanApiConstants.chargesParameterName).isEmpty(); if (jsonCommand.parameterExists(LoanApiConstants.disbursementDataParameterName)) { final JsonArray disbursementDataArray = jsonCommand.arrayOfParameterNamed(LoanApiConstants.disbursementDataParameterName); if (disbursementDataArray != null && disbursementDataArray.size() > 0) { String dateFormat = null; Locale locale = null; - // Gets date format and locate Map dateAndLocale = getDateFormatAndLocale(jsonCommand); dateFormat = dateAndLocale.get(LoanApiConstants.dateFormatParameterName); if (dateAndLocale.containsKey(LoanApiConstants.localeParameterName)) { @@ -1710,7 +1614,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { Long disbursementID = (Long) parsedDisbursementData.get(LoanApiConstants.disbursementIdParameterName); chargeIds = (String) parsedDisbursementData.get(LoanApiConstants.loanChargeIdParameterName); if (chargeIds != null) { - if (chargeIds.indexOf(",") != -1) { + if (chargeIds.contains(",")) { Iterable chargeId = Splitter.on(',').split(chargeIds); for (String loanChargeId : chargeId) { loanChargeIds.remove(Long.parseLong(loanChargeId)); @@ -1721,7 +1625,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } createOrUpdateDisbursementDetails(disbursementID, actualChanges, expectedDisbursementDate, principal, disbursementList); } - removeDisbursementAndAssociatedCharges(actualChanges, disbursementList, loanChargeIds, chargeIdLength, removeAllChages); + removeDisbursementAndAssociatedCharges(actualChanges, disbursementList, loanChargeIds, chargeIdLength, removeAllCharges); } } } @@ -1754,7 +1658,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { private void createOrUpdateDisbursementDetails(Long disbursementID, final Map actualChanges, LocalDate expectedDisbursementDate, BigDecimal principal, List existingDisbursementList) { - if (disbursementID != null) { LoanDisbursementDetails loanDisbursementDetail = fetchLoanDisbursementsById(disbursementID); existingDisbursementList.remove(disbursementID); @@ -1794,45 +1697,32 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } private void removeChargesByDisbursementID(Long id) { - List tempCharges = new ArrayList<>(); - for (LoanCharge charge : getCharges()) { - LoanTrancheDisbursementCharge transCharge = charge.getTrancheDisbursementCharge(); - if (transCharge != null && id.equals(transCharge.getloanDisbursementDetails().getId())) { - tempCharges.add(charge); - } - } - for (LoanCharge charge : tempCharges) { - removeLoanCharge(charge); - } + getCharges().stream() // + .filter(charge -> { // + LoanTrancheDisbursementCharge transCharge = charge.getTrancheDisbursementCharge(); // + return transCharge != null && id.equals(transCharge.getloanDisbursementDetails().getId()); // + }) // + .forEach(this::removeLoanCharge); } private List fetchLoanTrancheChargeIds() { - List list = new ArrayList<>(); - for (LoanCharge charge : getCharges()) { - if (charge.isTrancheDisbursementCharge() && charge.isActive()) { - list.add(charge.getId()); - } - } - return list; + return getCharges().stream()// + .filter(charge -> charge.isTrancheDisbursementCharge() && charge.isActive()) // + .map(LoanCharge::getId) // + .collect(Collectors.toList()); } public LoanDisbursementDetails fetchLoanDisbursementsById(Long id) { - LoanDisbursementDetails loanDisbursementDetail = null; - for (LoanDisbursementDetails disbursementDetail : getDisbursementDetails()) { - if (id.equals(disbursementDetail.getId())) { - loanDisbursementDetail = disbursementDetail; - break; - } - } - return loanDisbursementDetail; + return getDisbursementDetails().stream() // + .filter(disbursementDetail -> id.equals(disbursementDetail.getId())) // + .findFirst() // + .orElse(null); } private List fetchDisbursementIds() { - List list = new ArrayList<>(); - for (LoanDisbursementDetails disbursementDetails : getDisbursementDetails()) { - list.add(disbursementDetails.getId()); - } - return list; + return getDisbursementDetails().stream() // + .map(LoanDisbursementDetails::getId) // + .collect(Collectors.toList()); } private LocalDate determineExpectedMaturityDate() { @@ -1853,7 +1743,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { public Map loanApplicationRejection(final AppUser currentUser, final JsonCommand command, final LoanLifecycleStateMachine loanLifecycleStateMachine) { - validateAccountStatus(LoanEvent.LOAN_REJECTED); final Map actualChanges = new LinkedHashMap<>(); @@ -1901,7 +1790,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { public Map loanApplicationWithdrawnByApplicant(final AppUser currentUser, final JsonCommand command, final LoanLifecycleStateMachine loanLifecycleStateMachine) { - final Map actualChanges = new LinkedHashMap<>(); final LoanStatus statusEnum = loanLifecycleStateMachine.dryTransition(LoanEvent.LOAN_WITHDRAWN, this); @@ -1950,7 +1838,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { public Map loanApplicationApproval(final AppUser currentUser, final JsonCommand command, final JsonArray disbursementDataArray, final LoanLifecycleStateMachine loanLifecycleStateMachine) { - validateAccountStatus(LoanEvent.LOAN_APPROVED); final Map actualChanges = new LinkedHashMap<>(); @@ -1980,7 +1867,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { approvedOnDateChange = command.stringValueOfParameterNamed(EVENT_DATE); } - LocalDate expecteddisbursementDate = command.localDateValueOfParameterNamed(EXPECTED_DISBURSEMENT_DATE); + LocalDate expectedDisbursementDate = command.localDateValueOfParameterNamed(EXPECTED_DISBURSEMENT_DATE); BigDecimal approvedLoanAmount = command.bigDecimalValueOfParameterNamed(LoanApiConstants.approvedLoanAmountParameterName); if (approvedLoanAmount != null) { @@ -1997,7 +1884,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { actualChanges.put(LoanApiConstants.disbursementPrincipalParameterName, approvedLoanAmount); actualChanges.put(LoanApiConstants.disbursementNetDisbursalAmountParameterName, netDisbursalAmount); - /* Update disbursement details */ if (disbursementDataArray != null) { updateDisbursementDetails(command, actualChanges); } @@ -2006,21 +1892,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { recalculateAllCharges(); if (loanProduct.isMultiDisburseLoan()) { - List currentDisbursementDetails = getDisbursementDetails(); - if (loanProduct.isDisallowExpectedDisbursements()) { - if (!currentDisbursementDetails.isEmpty()) { - final String errorMessage = "For this loan product, disbursement details are not allowed"; - throw new MultiDisbursementDataNotAllowedException(LoanApiConstants.disbursementDataParameterName, errorMessage); - } - } else { - if (currentDisbursementDetails.isEmpty()) { - final String errorMessage = "For this loan product, disbursement details must be provided"; - throw new MultiDisbursementDataRequiredException(LoanApiConstants.disbursementDataParameterName, errorMessage); - } - } + List currentDisbursementDetails = getLoanDisbursementDetails(); if (currentDisbursementDetails.size() > loanProduct.maxTrancheCount()) { - final String errorMessage = "Number of tranche shouldn't be greter than " + loanProduct.maxTrancheCount(); + final String errorMessage = "Number of tranche shouldn't be greater than " + loanProduct.maxTrancheCount(); throw new ExceedingTrancheCountException(LoanApiConstants.disbursementDataParameterName, errorMessage, loanProduct.maxTrancheCount(), currentDisbursementDetails.size()); } @@ -2038,15 +1913,15 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { getApprovedOnDate(), submittalDate); } - if (expecteddisbursementDate != null) { - this.expectedDisbursementDate = expecteddisbursementDate; - actualChanges.put(EXPECTED_DISBURSEMENT_DATE, expectedDisbursementDate); + if (expectedDisbursementDate != null) { + this.expectedDisbursementDate = expectedDisbursementDate; + actualChanges.put(EXPECTED_DISBURSEMENT_DATE, this.expectedDisbursementDate); - if (DateUtils.isBefore(expecteddisbursementDate, approvedOn)) { + if (DateUtils.isBefore(expectedDisbursementDate, approvedOn)) { final String errorMessage = "The expected disbursement date should be either on or after the approval date: " - + approvedOn.toString(); + + approvedOn; throw new InvalidLoanStateTransitionException("expecteddisbursal", "should.be.on.or.after.approval.date", errorMessage, - getApprovedOnDate(), expecteddisbursementDate); + getApprovedOnDate(), expectedDisbursementDate); } } @@ -2066,11 +1941,25 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } return actualChanges; + } + private List getLoanDisbursementDetails() { + List currentDisbursementDetails = getDisbursementDetails(); + if (loanProduct.isDisallowExpectedDisbursements()) { + if (!currentDisbursementDetails.isEmpty()) { + final String errorMessage = "For this loan product, disbursement details are not allowed"; + throw new MultiDisbursementDataNotAllowedException(LoanApiConstants.disbursementDataParameterName, errorMessage); + } + } else { + if (currentDisbursementDetails.isEmpty()) { + final String errorMessage = "For this loan product, disbursement details must be provided"; + throw new MultiDisbursementDataRequiredException(LoanApiConstants.disbursementDataParameterName, errorMessage); + } + } + return currentDisbursementDetails; } private void compareApprovedToProposedPrincipal(BigDecimal approvedLoanAmount) { - if (this.loanProduct().isDisallowExpectedDisbursements() && this.loanProduct().isAllowApprovedDisbursedAmountsOverApplied()) { BigDecimal maxApprovedLoanAmount = getOverAppliedMax(); if (approvedLoanAmount.compareTo(maxApprovedLoanAmount) > 0) { @@ -2089,20 +1978,16 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } private BigDecimal getOverAppliedMax() { - BigDecimal maxAmount = null; - if (this.getLoanProduct().getOverAppliedCalculationType().equals("percentage")) { + if ("percentage".equals(getLoanProduct().getOverAppliedCalculationType())) { BigDecimal overAppliedNumber = BigDecimal.valueOf(getLoanProduct().getOverAppliedNumber()); - BigDecimal x = overAppliedNumber.divide(BigDecimal.valueOf(100)); - BigDecimal totalPercentage = BigDecimal.valueOf(1).add(x); - maxAmount = this.proposedPrincipal.multiply(totalPercentage); + BigDecimal totalPercentage = BigDecimal.valueOf(1).add(overAppliedNumber.divide(BigDecimal.valueOf(100))); + return proposedPrincipal.multiply(totalPercentage); } else { - maxAmount = this.proposedPrincipal.add(BigDecimal.valueOf(getLoanProduct().getOverAppliedNumber())); + return proposedPrincipal.add(BigDecimal.valueOf(getLoanProduct().getOverAppliedNumber())); } - return maxAmount; } public Map undoApproval(final LoanLifecycleStateMachine loanLifecycleStateMachine) { - validateAccountStatus(LoanEvent.LOAN_APPROVAL_UNDO); final Map actualChanges = new LinkedHashMap<>(); @@ -2133,31 +2018,20 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } public List findExistingTransactionIds() { - final List ids = new ArrayList<>(); - List transactions = getLoanTransactions(); - for (final LoanTransaction transaction : transactions) { - ids.add(transaction.getId()); - } - - return ids; + return getLoanTransactions().stream() // + .map(LoanTransaction::getId) // + .collect(Collectors.toList()); } public List findExistingReversedTransactionIds() { - - final List ids = new ArrayList<>(); - List transactions = getLoanTransactions(); - for (final LoanTransaction transaction : transactions) { - if (transaction.isReversed()) { - ids.add(transaction.getId()); - } - } - - return ids; + return getLoanTransactions().stream() // + .filter(LoanTransaction::isReversed) // + .map(LoanTransaction::getId) // + .collect(Collectors.toList()); } public ChangedTransactionDetail disburse(final AppUser currentUser, final JsonCommand command, final Map actualChanges, final ScheduleGeneratorDTO scheduleGeneratorDTO, final PaymentDetail paymentDetail) { - final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed(ACTUAL_DISBURSEMENT_DATE); this.disbursedBy = currentUser; @@ -2182,11 +2056,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { updateLoanSummaryDerivedFields(); final Money interestApplied = Money.of(getCurrency(), this.summary.getTotalInterestCharged()); - /** + /* * Add an interest applied transaction of the interest is accrued upfront (Up front accrual), no accounting or * cash based accounting is selected - **/ - + */ if (((isMultiDisburmentLoan() && getDisbursedLoanDisbursementDetails().size() == 1) || !isMultiDisburmentLoan()) && isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct()) { ExternalId externalId = ExternalId.empty(); @@ -2202,12 +2075,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { this.loanLifecycleStateMachine.transition(LoanEvent.LOAN_DISBURSED, this); actualChanges.put(PARAM_STATUS, LoanEnumerations.status(this.loanStatus)); return result; - } private void regenerateRepaymentScheduleWithInterestRecalculationIfNeeded(boolean interestRecalculationEnabledParam, boolean disbursementMissedParam, ScheduleGeneratorDTO scheduleGeneratorDTO) { - LocalDate firstInstallmentDueDate = fetchRepaymentScheduleInstallment(1).getDueDate(); if ((interestRecalculationEnabledParam && (DateUtils.isBeforeBusinessDate(firstInstallmentDueDate) || disbursementMissedParam))) { regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO); @@ -2215,15 +2086,9 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } private List getDisbursedLoanDisbursementDetails() { - List ret = new ArrayList<>(); - if (this.disbursementDetails != null && !this.disbursementDetails.isEmpty()) { - for (LoanDisbursementDetails disbursementDetail : getDisbursementDetails()) { - if (disbursementDetail.actualDisbursementDate() != null) { - ret.add(disbursementDetail); - } - } - } - return ret; + return getDisbursementDetails().stream() // + .filter(it -> it.actualDisbursementDate() != null) // + .collect(Collectors.toList()); } public void regenerateScheduleOnDisbursement(final ScheduleGeneratorDTO scheduleGeneratorDTO, final boolean recalculateSchedule, @@ -2342,7 +2207,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } private void compareDisbursedToApprovedOrProposedPrincipal(BigDecimal disbursedAmount, BigDecimal totalDisbursed) { - if (this.loanProduct().isDisallowExpectedDisbursements() && this.loanProduct().isAllowApprovedDisbursedAmountsOverApplied()) { BigDecimal maxDisbursedAmount = getOverAppliedMax(); if (totalDisbursed.compareTo(maxDisbursedAmount) > 0) { @@ -2379,7 +2243,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } updateLoanSummaryDerivedFields(); } - return changedTransactionDetail; } @@ -2418,15 +2281,9 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } private boolean isDisbursementMissed() { - boolean isDisbursementMissed = false; - for (LoanDisbursementDetails disbursementDetail : getDisbursementDetails()) { - if (disbursementDetail.actualDisbursementDate() == null - && DateUtils.isBeforeBusinessDate(disbursementDetail.expectedDisbursementDateAsLocalDate())) { - isDisbursementMissed = true; - break; - } - } - return isDisbursementMissed; + return getDisbursementDetails().stream() // + .anyMatch(disbursementDetail -> disbursementDetail.actualDisbursementDate() == null + && DateUtils.isBeforeBusinessDate(disbursementDetail.expectedDisbursementDateAsLocalDate())); } public BigDecimal getDisbursedAmount() { @@ -2440,39 +2297,17 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } private void removeDisbursementDetail() { - Set details = new HashSet<>(getDisbursementDetails()); - for (LoanDisbursementDetails disbursementDetail : details) { - if (disbursementDetail.actualDisbursementDate() == null) { - this.disbursementDetails.remove(disbursementDetail); - } - } + getDisbursementDetails().removeIf(it -> it.actualDisbursementDate() == null); } private boolean isDisbursementAllowed() { - boolean isAllowed = false; List disbursementDetails = getDisbursementDetails(); - if (disbursementDetails == null || disbursementDetails.isEmpty()) { - isAllowed = true; - } else { - for (LoanDisbursementDetails disbursementDetail : disbursementDetails) { - if (disbursementDetail.actualDisbursementDate() == null) { - isAllowed = true; - break; - } - } - } - return isAllowed; + return disbursementDetails == null || disbursementDetails.isEmpty() + || disbursementDetails.stream().anyMatch(it -> it.actualDisbursementDate() == null); } - private boolean atleastOnceDisbursed() { - boolean isDisbursed = false; - for (LoanDisbursementDetails disbursementDetail : getDisbursementDetails()) { - if (disbursementDetail.actualDisbursementDate() != null) { - isDisbursed = true; - break; - } - } - return isDisbursed; + private boolean atLeastOnceDisbursed() { + return getDisbursementDetails().stream().anyMatch(it -> it.actualDisbursementDate() != null); } private void updateLoanRepaymentPeriodsDerivedFields(final LocalDate actualDisbursementDate) { @@ -2482,7 +2317,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } } - /* + /** * Ability to regenerate the repayment schedule based on the loans current details/state. */ public void regenerateRepaymentSchedule(final ScheduleGeneratorDTO scheduleGeneratorDTO) { @@ -2500,7 +2335,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } public LoanScheduleModel regenerateScheduleModel(final ScheduleGeneratorDTO scheduleGeneratorDTO) { - final MathContext mc = MoneyHelper.getMathContext(); final LoanApplicationTerms loanApplicationTerms = constructLoanApplicationTerms(scheduleGeneratorDTO); @@ -2551,19 +2385,18 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } private void handleDisbursementTransaction(final LocalDate disbursedOn, final PaymentDetail paymentDetail) { - // add repayment transaction to track incoming money from client to mfi // for (charges due at time of disbursement) - /*** + /* * TODO Vishwas: do we need to be able to pass in payment type details for repayments at disbursements too? - ***/ + */ final Money totalFeeChargesDueAtDisbursement = this.summary.getTotalFeeChargesDueAtDisbursement(loanCurrency()); - /** + /* * all Charges repaid at disbursal is marked as repaid and "APPLY Charge" transactions are created for all other * fees ( which are created during disbursal but not repaid) - **/ + */ Money disbursentMoney = Money.zero(getCurrency()); final LoanTransaction chargesPayment = LoanTransaction.repaymentAtDisbursement(getOffice(), disbursentMoney, paymentDetail, @@ -2571,9 +2404,9 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { final Integer installmentNumber = null; for (final LoanCharge charge : getActiveCharges()) { LocalDate actualDisbursementDate = getActualDisbursementDate(charge); - /** + /* * create a Charge applied transaction if Up front Accrual, None or Cash based accounting is enabled - **/ + */ if ((charge.getCharge().getChargeTimeType().equals(ChargeTimeType.DISBURSEMENT.getValue()) && disbursedOn.equals(actualDisbursementDate) && (actualDisbursementDate != null) && !charge.isWaived() && !charge.isFullyPaid()) @@ -2624,7 +2457,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { + " is disbursed cannot be in the future."; throw new InvalidLoanStateTransitionException("disbursal", "cannot.be.a.future.date", errorMessage, disbursedOn); } - } public LoanTransaction handleDownPayment(final LoanTransaction disbursementTransaction, final JsonCommand command, @@ -2658,6 +2490,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { case PROGRESSIVE -> MathUtil.negativeToZero(downPaymentMoney.minus(disbursementTransaction.getOverPaymentPortion(getCurrency()))); }; + if (adjustedDownPaymentMoney.isGreaterThanZero()) { LoanTransaction downPaymentTransaction = LoanTransaction.downPayment(getOffice(), adjustedDownPaymentMoney, null, disbursedOn, externalId); @@ -2681,7 +2514,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { && this.loanRepaymentScheduleDetail.isEnableAutoRepaymentForDownPayment(); } - public LoanTransaction handlePayDisbursementTransaction(final Long chargeId, final LoanTransaction chargesPayment, + public void handlePayDisbursementTransaction(final Long chargeId, final LoanTransaction chargesPayment, final List existingTransactionIds, final List existingReversedTransactionIds) { existingTransactionIds.addAll(findExistingTransactionIds()); existingReversedTransactionIds.addAll(findExistingReversedTransactionIds()); @@ -2691,7 +2524,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { charge = loanCharge; } } - @SuppressWarnings("null") final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(chargesPayment, charge, charge.amount(), null); chargesPayment.getLoanChargesPaid().add(loanChargePaidBy); final Money zero = Money.zero(getCurrency()); @@ -2700,17 +2532,14 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { addLoanTransaction(chargesPayment); updateLoanOutstandingBalances(); charge.markAsFullyPaid(); - return chargesPayment; } public void removePostDatedChecks() { - List postDatedChecks = new ArrayList<>(); - this.postDatedChecks = postDatedChecks; + this.postDatedChecks = new ArrayList<>(); } public Map undoDisbursal(final ScheduleGeneratorDTO scheduleGeneratorDTO, final List existingTransactionIds, final List existingReversedTransactionIds) { - validateAccountStatus(LoanEvent.LOAN_DISBURSAL_UNDO); final Map actualChanges = new LinkedHashMap<>(); @@ -2764,11 +2593,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } this.adjustNetDisbursalAmount(this.approvedPrincipal); - actualChanges.put(ACTUAL_DISBURSEMENT_DATE, ""); - updateLoanSummaryDerivedFields(); - } return actualChanges; @@ -2816,9 +2642,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { public ChangedTransactionDetail waiveInterest(final LoanTransaction waiveInterestTransaction, final LoanLifecycleStateMachine loanLifecycleStateMachine, final List existingTransactionIds, final List existingReversedTransactionIds, final ScheduleGeneratorDTO scheduleGeneratorDTO) { - validateAccountStatus(LoanEvent.LOAN_REPAYMENT_OR_WAIVER); - validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, waiveInterestTransaction.getTransactionDate()); validateActivityNotBeforeLastTransactionDate(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, waiveInterestTransaction.getTransactionDate()); @@ -2835,13 +2659,9 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { final LoanLifecycleStateMachine loanLifecycleStateMachine, final List existingTransactionIds, final List existingReversedTransactionIds, boolean isRecoveryRepayment, final ScheduleGeneratorDTO scheduleGeneratorDTO, Boolean isHolidayValidationDone) { + LoanEvent event = isRecoveryRepayment ? LoanEvent.LOAN_RECOVERY_PAYMENT : LoanEvent.LOAN_REPAYMENT_OR_WAIVER; + HolidayDetailDTO holidayDetailDTO = null; - LoanEvent event = null; - if (isRecoveryRepayment) { - event = LoanEvent.LOAN_RECOVERY_PAYMENT; - } else { - event = LoanEvent.LOAN_REPAYMENT_OR_WAIVER; - } if (!isHolidayValidationDone) { holidayDetailDTO = scheduleGeneratorDTO.getHolidayDetailDTO(); } @@ -2865,7 +2685,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { if (repaymentTransaction.isGoodwillCredit() || repaymentTransaction.isMerchantIssuedRefund() || repaymentTransaction.isPayoutRefund() || repaymentTransaction.isChargeRefund() || repaymentTransaction.isRepayment() || repaymentTransaction.isDownPayment()) { - if (!(isOpen() || isClosedObligationsMet() || isOverPaid())) { final List dataValidationErrors = new ArrayList<>(); final String defaultUserMessage = "Loan must be Active, Fully Paid or Overpaid"; @@ -2883,7 +2702,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { public void makeChargePayment(final Long chargeId, final LoanLifecycleStateMachine loanLifecycleStateMachine, final List existingTransactionIds, final List existingReversedTransactionIds, final HolidayDetailDTO holidayDetailDTO, final LoanTransaction paymentTransaction, final Integer installmentNumber) { - validateAccountStatus(LoanEvent.LOAN_CHARGE_PAYMENT); validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_CHARGE_PAYMENT, paymentTransaction.getTransactionDate()); validateActivityNotBeforeLastTransactionDate(LoanEvent.LOAN_CHARGE_PAYMENT, paymentTransaction.getTransactionDate()); @@ -2912,7 +2730,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { final List existingTransactionIds, final List existingReversedTransactionIds, final boolean allowTransactionsOnHoliday, final List holidays, final WorkingDays workingDays, final boolean allowTransactionsOnNonWorkingDay) { - validateRepaymentDateIsOnHoliday(loanTransaction.getTransactionDate(), allowTransactionsOnHoliday, holidays); validateRepaymentDateIsOnNonWorkingDay(loanTransaction.getTransactionDate(), workingDays, allowTransactionsOnNonWorkingDay); @@ -2924,7 +2741,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { final String errorMessage = "The refund amount must be less than or equal to overpaid amount "; throw new InvalidLoanStateTransitionException("transaction", "is.exceeding.overpaid.amount", errorMessage, this.totalOverpaid, loanTransaction.getAmount(getCurrency()).getAmount()); - } else if (!isAfterLatRepayment(loanTransaction, getLoanTransactions())) { + } else if (!isAfterLastRepayment(loanTransaction, getLoanTransactions())) { final String errorMessage = "Transfer funds is allowed only after last repayment date"; throw new InvalidLoanStateTransitionException("transaction", "is.not.after.repayment.date", errorMessage); } @@ -2945,7 +2762,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { private ChangedTransactionDetail handleRepaymentOrRecoveryOrWaiverTransaction(final LoanTransaction loanTransaction, final LoanLifecycleStateMachine loanLifecycleStateMachine, final LoanTransaction adjustedTransaction, final ScheduleGeneratorDTO scheduleGeneratorDTO) { - ChangedTransactionDetail changedTransactionDetail = null; if (loanTransaction.isRecoveryRepayment()) { @@ -2972,13 +2788,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { errorMessage); } - final LocalDate loanTransactionDate = loanTransaction.getTransactionDate(); - if (DateUtils.isBefore(loanTransactionDate, getDisbursementDate())) { - final String errorMessage = "The transaction date cannot be before the loan disbursement date: " - + getDisbursementDate().toString(); - throw new InvalidLoanStateTransitionException("transaction", "cannot.be.before.disbursement.date", errorMessage, - loanTransactionDate, getDisbursementDate()); - } + final LocalDate loanTransactionDate = extractTransactionDate(loanTransaction); if (DateUtils.isDateInTheFuture(loanTransactionDate)) { final String errorMessage = "The transaction date cannot be in the future."; @@ -3012,13 +2822,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { final LoanRepaymentScheduleInstallment currentInstallment = fetchLoanRepaymentScheduleInstallment( loanTransaction.getTransactionDate()); - boolean reprocess = true; - if (!isForeclosure() && isTransactionChronologicallyLatest && adjustedTransaction == null - && DateUtils.isEqualBusinessDate(loanTransaction.getTransactionDate()) && currentInstallment != null - && currentInstallment.getTotalOutstanding(getCurrency()).isEqualTo(loanTransaction.getAmount(getCurrency()))) { - reprocess = false; - } + boolean reprocess = isForeclosure() || !isTransactionChronologicallyLatest || adjustedTransaction != null + || !DateUtils.isEqualBusinessDate(loanTransaction.getTransactionDate()) || currentInstallment == null + || !currentInstallment.getTotalOutstanding(getCurrency()).isEqualTo(loanTransaction.getAmount(getCurrency())); if (isTransactionChronologicallyLatest && adjustedTransaction == null && (!reprocess || !this.repaymentScheduleDetail().isInterestRecalculationEnabled()) && !isForeclosure()) { @@ -3047,7 +2854,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { mapEntry.getValue().updateLoan(this); } - /*** + /* * Commented since throwing exception if external id present for one of the transactions. for this need to * save the reversed transactions first and then new transactions. */ @@ -3077,53 +2884,42 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return changedTransactionDetail; } - private LoanRepaymentScheduleInstallment fetchLoanRepaymentScheduleInstallment(LocalDate dueDate) { - LoanRepaymentScheduleInstallment installment = null; - List installments = getRepaymentScheduleInstallments(); - for (LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : installments) { - if (dueDate.equals(loanRepaymentScheduleInstallment.getDueDate())) { - installment = loanRepaymentScheduleInstallment; - break; - } + private LocalDate extractTransactionDate(LoanTransaction loanTransaction) { + final LocalDate loanTransactionDate = loanTransaction.getTransactionDate(); + if (DateUtils.isBefore(loanTransactionDate, getDisbursementDate())) { + final String errorMessage = "The transaction date cannot be before the loan disbursement date: " + + getDisbursementDate().toString(); + throw new InvalidLoanStateTransitionException("transaction", "cannot.be.before.disbursement.date", errorMessage, + loanTransactionDate, getDisbursementDate()); } - return installment; + return loanTransactionDate; + } + + private LoanRepaymentScheduleInstallment fetchLoanRepaymentScheduleInstallment(LocalDate dueDate) { + return getRepaymentScheduleInstallments().stream() // + .filter(installment -> dueDate.equals(installment.getDueDate())).findFirst() // + .orElse(null); } private List retrieveListOfIncomePostingTransactions() { - final List incomePostTransactions = new ArrayList<>(); - List trans = getLoanTransactions(); - for (final LoanTransaction transaction : trans) { - if (transaction.isNotReversed() && transaction.isIncomePosting()) { - incomePostTransactions.add(transaction); - } - } - incomePostTransactions.sort(LoanTransactionComparator.INSTANCE); - return incomePostTransactions; + return getLoanTransactions().stream() // + .filter(transaction -> transaction.isNotReversed() && transaction.isIncomePosting()) // + .sorted(LoanTransactionComparator.INSTANCE).collect(Collectors.toList()); } public List retrieveListOfTransactionsForReprocessing() { - final List repaymentsOrWaivers = new ArrayList<>(); - List trans = getLoanTransactions(); - for (final LoanTransaction transaction : trans) { - if (transaction.isNotReversed() && !transaction.isAccrual() && (transaction.isChargeOff() || transaction.isReAge() - || transaction.isReAmortize() || !transaction.isNonMonetaryTransaction())) { - repaymentsOrWaivers.add(transaction); - } - } - repaymentsOrWaivers.sort(LoanTransactionComparator.INSTANCE); - return repaymentsOrWaivers; + return getLoanTransactions().stream() + .filter(transaction -> transaction.isNotReversed() && !transaction.isAccrual() + && (transaction.isChargeOff() || transaction.isReAge() || transaction.isReAmortize() + || !transaction.isNonMonetaryTransaction())) + .sorted(LoanTransactionComparator.INSTANCE).collect(Collectors.toList()); } public List retrieveListOfTransactionsPostDisbursementExcludeAccruals() { - final List repaymentsOrWaivers = new ArrayList<>(); - for (final LoanTransaction transaction : this.loanTransactions) { - if (transaction.isNotReversed() && !(transaction.isAccrual() || transaction.isRepaymentAtDisbursement() - || transaction.isNonMonetaryTransaction() || transaction.isIncomePosting())) { - repaymentsOrWaivers.add(transaction); - } - } - repaymentsOrWaivers.sort(LoanTransactionComparator.INSTANCE); - return repaymentsOrWaivers; + return this.loanTransactions.stream() + .filter(transaction -> transaction.isNotReversed() && !(transaction.isAccrual() || transaction.isRepaymentAtDisbursement() + || transaction.isNonMonetaryTransaction() || transaction.isIncomePosting())) + .sorted(LoanTransactionComparator.INSTANCE).collect(Collectors.toList()); } private List retrieveListOfTransactionsExcludeAccruals() { @@ -3138,25 +2934,14 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } private List retrieveListOfAccrualTransactions() { - final List transactions = new ArrayList<>(); - for (final LoanTransaction transaction : this.loanTransactions) { - if (transaction.isNotReversed() && transaction.isAccrual()) { - transactions.add(transaction); - } - } - transactions.sort(LoanTransactionComparator.INSTANCE); - return transactions; + return this.loanTransactions.stream().filter(transaction -> transaction.isNotReversed() && transaction.isAccrual()) + .sorted(LoanTransactionComparator.INSTANCE).collect(Collectors.toList()); } public List retrieveListOfTransactionsByType(final LoanTransactionType transactionType) { - final List transactions = new ArrayList<>(); - for (final LoanTransaction transaction : this.loanTransactions) { - if (transaction.isNotReversed() && transaction.getTypeOf().equals(transactionType)) { - transactions.add(transaction); - } - } - transactions.sort(LoanTransactionComparator.INSTANCE); - return transactions; + return this.loanTransactions.stream() + .filter(transaction -> transaction.isNotReversed() && transaction.getTypeOf().equals(transactionType)) + .sorted(LoanTransactionComparator.INSTANCE).collect(Collectors.toList()); } private boolean doPostLoanTransactionChecks(final LocalDate transactionDate, @@ -3180,15 +2965,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } private void handleLoanRepaymentInFull(final LocalDate transactionDate, final LoanLifecycleStateMachine loanLifecycleStateMachine) { + boolean isAllChargesPaid = this.charges.stream() // + .allMatch(charge -> !charge.isActive() || charge.amount().compareTo(BigDecimal.ZERO) <= 0 || charge.isPaid() + || charge.isWaived()); - boolean isAllChargesPaid = true; - for (final LoanCharge loanCharge : this.charges) { - if (loanCharge.isActive() && loanCharge.amount().compareTo(BigDecimal.ZERO) > 0 - && !(loanCharge.isPaid() || loanCharge.isWaived())) { - isAllChargesPaid = false; - break; - } - } if (isAllChargesPaid) { this.closedOnDate = transactionDate; this.actualMaturityDate = transactionDate; @@ -3304,43 +3084,28 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return isChronologicallyLatestRepaymentOrWaiver; } - private boolean isAfterLatRepayment(final LoanTransaction loanTransaction, final List loanTransactions) { - boolean isAfterLatRepayment = true; - - final LocalDate currentTransactionDate = loanTransaction.getTransactionDate(); - for (final LoanTransaction previousTransaction : loanTransactions) { - if (previousTransaction.isRepaymentLikeType() && previousTransaction.isNotReversed() - && DateUtils.isBefore(currentTransactionDate, previousTransaction.getTransactionDate())) { - isAfterLatRepayment = false; - break; - } - } - return isAfterLatRepayment; + private boolean isAfterLastRepayment(final LoanTransaction loanTransaction, final List loanTransactions) { + return loanTransactions.stream() // + .filter(t -> t.isRepaymentLikeType() && t.isNotReversed()) // + .noneMatch(t -> DateUtils.isBefore(loanTransaction.getTransactionDate(), t.getTransactionDate())); } private boolean isChronologicallyLatestTransaction(final LoanTransaction loanTransaction, final List loanTransactions) { - boolean isChronologicallyLatestRepaymentOrWaiver = true; - - final LocalDate currentTransactionDate = loanTransaction.getTransactionDate(); - for (final LoanTransaction previousTransaction : loanTransactions) { - if (previousTransaction.isNotReversed() - && !DateUtils.isAfter(currentTransactionDate, previousTransaction.getTransactionDate())) { - isChronologicallyLatestRepaymentOrWaiver = false; - break; - } - } - return isChronologicallyLatestRepaymentOrWaiver; + return loanTransactions.stream() // + .filter(LoanTransaction::isNotReversed) // + .allMatch(t -> DateUtils.isAfter(loanTransaction.getTransactionDate(), t.getTransactionDate())); } public LocalDate possibleNextRepaymentDate(final String nextPaymentDueDateConfig) { - LocalDate nextPossibleRepaymentDate = null; - if (EARLIEST_UNPAID_DATE.equalsIgnoreCase(nextPaymentDueDateConfig)) { - nextPossibleRepaymentDate = getEarliestUnpaidInstallmentDate(); - } else if (NEXT_UNPAID_DUE_DATE.equalsIgnoreCase(nextPaymentDueDateConfig)) { - nextPossibleRepaymentDate = getNextUnpaidInstallmentDueDate(); + if (nextPaymentDueDateConfig == null) { + return null; } - return nextPossibleRepaymentDate; + return switch (nextPaymentDueDateConfig.toLowerCase()) { + case EARLIEST_UNPAID_DATE -> getEarliestUnpaidInstallmentDate(); + case NEXT_UNPAID_DUE_DATE -> getNextUnpaidInstallmentDueDate(); + default -> null; + }; } private LocalDate getNextUnpaidInstallmentDueDate() { @@ -3391,21 +3156,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return possibleNextRepaymentDate; } - public LoanRepaymentScheduleInstallment possibleNextRepaymentInstallment() { - LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = null; - List installments = getRepaymentScheduleInstallments(); - for (final LoanRepaymentScheduleInstallment installment : installments) { - if (installment.isNotFullyPaidOff()) { - loanRepaymentScheduleInstallment = installment; - break; - } - } - - return loanRepaymentScheduleInstallment; - } - public LoanTransaction deriveDefaultInterestWaiverTransaction() { - final Money totalInterestOutstanding = getTotalInterestOutstandingOnLoan(); Money possibleInterestToWaive = totalInterestOutstanding.copy(); LocalDate transactionDate = DateUtils.getBusinessLocalDate(); @@ -3434,7 +3185,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { final LoanLifecycleStateMachine loanLifecycleStateMachine, final LoanTransaction transactionForAdjustment, final List existingTransactionIds, final List existingReversedTransactionIds, final ScheduleGeneratorDTO scheduleGeneratorDTO, final ExternalId reversalExternalId) { - HolidayDetailDTO holidayDetailDTO = scheduleGeneratorDTO.getHolidayDetailDTO(); validateActivityNotBeforeLastTransactionDate(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, transactionForAdjustment.getTransactionDate()); validateRepaymentDateIsOnHoliday(newTransactionDetail.getTransactionDate(), holidayDetailDTO.isAllowTransactionsOnHoliday(), @@ -3466,7 +3216,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { writeOffTransaction.reverse(); } - if (isClosedObligationsMet() || isClosedWrittenOff() || isClosedWithOutsandingAmountMarkedForReschedule()) { + if (isClosedObligationsMet() || isClosedWrittenOff() || isClosedWithOutstandingAmountMarkedForReschedule()) { loanLifecycleStateMachine.transition(LoanEvent.LOAN_ADJUST_TRANSACTION, this); } @@ -3481,7 +3231,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { public ChangedTransactionDetail undoWrittenOff(LoanLifecycleStateMachine loanLifecycleStateMachine, final List existingTransactionIds, final List existingReversedTransactionIds, final ScheduleGeneratorDTO scheduleGeneratorDTO) { - validateAccountStatus(LoanEvent.WRITE_OFF_OUTSTANDING_UNDO); existingTransactionIds.addAll(findExistingTransactionIds()); existingReversedTransactionIds.addAll(findExistingReversedTransactionIds()); @@ -3502,15 +3251,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } public LoanTransaction findWriteOffTransaction() { - - LoanTransaction writeOff = null; - for (final LoanTransaction transaction : this.loanTransactions) { - if (!transaction.isReversed() && transaction.isWriteOff()) { - writeOff = transaction; - } - } - - return writeOff; + return this.loanTransactions.stream() // + .filter(transaction -> !transaction.isReversed() && transaction.isWriteOff()) // + .findFirst() // + .orElse(null); } private boolean isOverPaid() { @@ -3518,7 +3262,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } private Money calculateTotalOverpayment() { - Money totalPaidInRepayments = getTotalPaidInRepayments(); final MonetaryCurrency currency = loanCurrency(); @@ -3526,7 +3269,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { Money cumulativeTotalWaivedOnInstallments = Money.zero(currency); List installments = getRepaymentScheduleInstallments(); for (final LoanRepaymentScheduleInstallment scheduledRepayment : installments) { - cumulativeTotalPaidOnInstallments = cumulativeTotalPaidOnInstallments .plus(scheduledRepayment.getPrincipalCompleted(currency).plus(scheduledRepayment.getInterestPaid(currency))) .plus(scheduledRepayment.getFeeChargesPaid(currency)).plus(scheduledRepayment.getPenaltyChargesPaid(currency)); @@ -3545,21 +3287,21 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { totalPaidInRepayments = totalPaidInRepayments.minus(loanTransaction.getOverPaymentPortion(currency)); } } else if (loanTransaction.isChargeback()) { - if (loanTransaction.getPrincipalPortion(currency).isZero() && getCreditAllocationRules().stream() - .filter(car -> car.getTransactionType().equals(CreditAllocationTransactionType.CHARGEBACK)).findAny().isEmpty()) { + if (loanTransaction.getPrincipalPortion(currency).isZero() && getCreditAllocationRules().stream() // + .filter(car -> car.getTransactionType().equals(CreditAllocationTransactionType.CHARGEBACK)) // + .findAny() // + .isEmpty()) { totalPaidInRepayments = totalPaidInRepayments.minus(loanTransaction.getOverPaymentPortion(currency)); } } } - // if total paid in transactions doesnt match repayment schedule then - // theres an overpayment. + // if total paid in transactions doesnt match repayment schedule then there's an overpayment. return totalPaidInRepayments.minus(cumulativeTotalPaidOnInstallments); } public Money calculateTotalRecoveredPayments() { - // in case logic for reversing recovered payment is implemented handle - // subtraction from totalRecoveredPayments + // in case logic for reversing recovered payment is implemented handle subtraction from totalRecoveredPayments return getTotalRecoveredPayments(); } @@ -3642,7 +3384,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { private ChangedTransactionDetail closeDisbursements(final ScheduleGeneratorDTO scheduleGeneratorDTO, final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor) { ChangedTransactionDetail changedTransactionDetail = null; - if (isDisbursementAllowed() && atleastOnceDisbursed()) { + if (isDisbursementAllowed() && atLeastOnceDisbursed()) { this.loanRepaymentScheduleDetail.setPrincipal(getDisbursedAmount()); removeDisbursementDetail(); regenerateRepaymentSchedule(scheduleGeneratorDTO); @@ -3657,21 +3399,18 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { addLoanTransaction(mapEntry.getValue()); } updateLoanSummaryDerivedFields(); - LoanTransaction loanTransaction = getLatestTransaction(); - doPostLoanTransactionChecks(loanTransaction.getTransactionDate(), loanLifecycleStateMachine); + LocalDate lastLoanTransactionDate = getLatestTransactionDate(); + doPostLoanTransactionChecks(lastLoanTransactionDate, loanLifecycleStateMachine); } return changedTransactionDetail; } - public LoanTransaction getLatestTransaction() { - LoanTransaction transaction = null; - for (LoanTransaction loanTransaction : this.loanTransactions) { - if (!loanTransaction.isReversed() && (transaction == null - || DateUtils.isBefore(transaction.getTransactionDate(), loanTransaction.getTransactionDate()))) { - transaction = loanTransaction; - } - } - return transaction; + public LocalDate getLatestTransactionDate() { + LoanTransaction oneOfTheLatestTxn = this.loanTransactions.stream() // + .filter(loanTransaction -> !loanTransaction.isReversed()) // + .max(Comparator.comparing(LoanTransaction::getTransactionDate)) // + .orElse(null); + return oneOfTheLatestTxn != null ? oneOfTheLatestTxn.getTransactionDate() : null; } public ChangedTransactionDetail close(final JsonCommand command, final LoanLifecycleStateMachine loanLifecycleStateMachine, @@ -3773,7 +3512,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { */ public void closeAsMarkedForReschedule(final JsonCommand command, final LoanLifecycleStateMachine loanLifecycleStateMachine, final Map changes) { - final LocalDate rescheduledOn = command.localDateValueOfParameterNamed(TRANSACTION_DATE); this.closedOnDate = rescheduledOn; @@ -3826,13 +3564,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } public boolean isChargesAdditionAllowed() { - boolean isDisbursed = false; - if (this.loanProduct.isMultiDisburseLoan()) { - isDisbursed = !isDisbursementAllowed(); - } else { - isDisbursed = hasDisbursementTransaction(); - } - return isDisbursed; + return this.loanProduct.isMultiDisburseLoan() ? !isDisbursementAllowed() : hasDisbursementTransaction(); } public boolean isDisbursed() { @@ -3851,7 +3583,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return getStatus().isClosedWrittenOff(); } - private boolean isClosedWithOutsandingAmountMarkedForReschedule() { + private boolean isClosedWithOutstandingAmountMarkedForReschedule() { return getStatus().isClosedWithOutsandingAmountMarkedForReschedule(); } @@ -3878,28 +3610,15 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } private boolean hasDisbursementTransaction() { - boolean hasRepaymentTransaction = false; - for (final LoanTransaction loanTransaction : this.loanTransactions) { - if (loanTransaction.isDisbursement() && loanTransaction.isNotReversed()) { - hasRepaymentTransaction = true; - break; - } - } - return hasRepaymentTransaction; + return this.loanTransactions.stream() + .anyMatch(loanTransaction -> loanTransaction.isDisbursement() && loanTransaction.isNotReversed()); + } public boolean isSubmittedOnDateAfter(final LocalDate compareDate) { return DateUtils.isAfter(this.submittedOnDate, compareDate); } - public LocalDate getSubmittedOnDate() { - return this.submittedOnDate; - } - - public LocalDate getApprovedOnDate() { - return this.approvedOnDate; - } - public LocalDate getExpectedDisbursedOnLocalDate() { return this.expectedDisbursementDate; } @@ -3912,14 +3631,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return disbursementDate; } - public void setActualDisbursementDate(LocalDate actualDisbursementDate) { - this.actualDisbursementDate = actualDisbursementDate; - } - - public LocalDate getWrittenOffDate() { - return this.writtenOffOnDate; - } - public LocalDate getExpectedDisbursedOnLocalDateForTemplate() { LocalDate expectedDisbursementDate = null; if (this.expectedDisbursementDate != null) { @@ -3947,10 +3658,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return principal; } - public LocalDate getExpectedFirstRepaymentOnDate() { - return this.expectedFirstRepaymentOnDate; - } - private boolean isActualDisbursedOnDateEarlierOrLaterThanExpected(final LocalDate actualDisbursedOnDate) { boolean isRegenerationRequired = false; if (this.loanProduct.isMultiDisburseLoan()) { @@ -4031,19 +3738,11 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } public boolean hasLoanOfficer(final Staff fromLoanOfficer) { - - boolean matchesCurrentLoanOfficer = false; if (this.loanOfficer != null) { - matchesCurrentLoanOfficer = this.loanOfficer.identifiedBy(fromLoanOfficer); + return this.loanOfficer.identifiedBy(fromLoanOfficer); } else { - matchesCurrentLoanOfficer = fromLoanOfficer == null; + return fromLoanOfficer == null; } - - return matchesCurrentLoanOfficer; - } - - public LocalDate getInterestChargedFromDate() { - return this.interestChargedFromDate; } public Money getPrincipal() { @@ -4090,8 +3789,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { throw new LoanOfficerAssignmentDateException("is.before.last.assignment.date", errorMessage, getId(), assignmentDate); } else { if (latestHistoryRecord != null) { - // loan officer correctly changed from previous loan officer to - // new loan officer + // loan officer correctly changed from previous loan officer to new loan officer latestHistoryRecord.updateEndDate(assignmentDate); } @@ -4105,7 +3803,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } public void removeLoanOfficer(final LocalDate unassignDate) { - final LoanOfficerAssignmentHistory latestHistoryRecord = findLatestIncompleteHistoryRecord(); if (latestHistoryRecord != null) { @@ -4158,47 +3855,23 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } public Long getClientId() { - Long clientId = null; - if (this.client != null) { - clientId = this.client.getId(); - } - return clientId; + return this.client == null ? null : this.client.getId(); } public Long getGroupId() { - Long groupId = null; - if (this.group != null) { - groupId = this.group.getId(); - } - return groupId; + return this.group == null ? null : this.group.getId(); } public Long getGlimId() { - Long glimId = null; - if (this.glim != null) { - glimId = this.glim.getId(); - } - return glimId; + return this.glim == null ? null : this.glim.getId(); } public Long getOfficeId() { - Long officeId = null; - if (this.client != null) { - officeId = this.client.officeId(); - } else { - officeId = this.group.officeId(); - } - return officeId; + return this.client != null ? this.client.officeId() : this.group.officeId(); } public Office getOffice() { - Office office = null; - if (this.client != null) { - office = this.client.getOffice(); - } else { - office = this.group.getOffice(); - } - return office; + return this.client != null ? this.client.getOffice() : this.group.getOffice(); } private Boolean isCashBasedAccountingEnabledOnLoanProduct() { @@ -4226,18 +3899,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return this.loanProduct.getId(); } - public Staff getLoanOfficer() { - return this.loanOfficer; - } - - public Set getCollateral() { - return this.collateral; - } - - public BigDecimal getProposedPrincipal() { - return this.proposedPrincipal; - } - public List> deriveAccountingBridgeDataForChargeOff(final String currencyCode, final List existingTransactionIds, final List existingReversedTransactionIds, boolean isAccountTransfer) { @@ -4311,24 +3972,29 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { private void filterTransactionsByChargeOffDate(List> filteredTransactions, final String currencyCode, final List existingTransactionIds, final List existingReversedTransactionIds, Predicate chargeOffDateCriteria) { - filteredTransactions.addAll(this.loanTransactions.stream().filter(chargeOffDateCriteria).filter(transaction -> { - boolean isExistingTransaction = existingTransactionIds.contains(transaction.getId()); - boolean isExistingReversedTransaction = existingReversedTransactionIds.contains(transaction.getId()); + filteredTransactions.addAll(this.loanTransactions.stream() // + .filter(chargeOffDateCriteria) // + .filter(transaction -> { + boolean isExistingTransaction = existingTransactionIds.contains(transaction.getId()); + boolean isExistingReversedTransaction = existingReversedTransactionIds.contains(transaction.getId()); - if (transaction.isReversed() && isExistingTransaction && !isExistingReversedTransaction) { - return true; - } else { - return !isExistingTransaction; - } - }).map(transaction -> transaction.toMapData(currencyCode)).toList()); + if (transaction.isReversed() && isExistingTransaction && !isExistingReversedTransaction) { + return true; + } else { + return !isExistingTransaction; + } + }) // + .map(transaction -> transaction.toMapData(currencyCode)).toList()); } private void filterTransactionsByChargeOffDate(List> newLoanTransactionsBeforeChargeOff, List> newLoanTransactionsAfterChargeOff, String currencyCode, List existingTransactionIds, List existingReversedTransactionIds, Predicate chargeOffDateCriteria) { - LoanTransaction chargeOffTransaction = this.loanTransactions.stream().filter(LoanTransaction::isChargeOff) - .filter(LoanTransaction::isNotReversed).findFirst().get(); + LoanTransaction chargeOffTransaction = this.loanTransactions.stream() // + .filter(LoanTransaction::isChargeOff) // + .filter(LoanTransaction::isNotReversed) // + .findFirst().get(); LoanTransaction originalChargeOffTransaction = getOriginalTransactionIfReverseReplayed(chargeOffTransaction); @@ -4428,10 +4094,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return this.syncDisbursementWithMeeting != null && this.syncDisbursementWithMeeting; } - public LocalDate getClosedOnDate() { - return this.closedOnDate; - } - public void updateLoanRepaymentScheduleDates(final String recurringRule, final boolean isHolidayEnabled, final List holidays, final WorkingDays workingDays, final LocalDate presentMeetingDate, final LocalDate newMeetingDate, final boolean isSkipRepaymentOnFirstDayOfMonth, final Integer numberOfDays) { @@ -4443,8 +4105,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { final Integer loanRepaymentInterval = this.loanRepaymentScheduleDetail.getRepayEvery(); final String frequency = CalendarUtils.getMeetingFrequencyFromPeriodFrequencyType(repaymentPeriodFrequencyType); - LocalDate newRepaymentDate = null; - Boolean isFirstTime = true; + LocalDate newRepaymentDate; + boolean isFirstTime = true; LocalDate latestRepaymentDate = null; List installments = getRepaymentScheduleInstallments(); for (final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : installments) { @@ -4460,6 +4122,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { newRepaymentDate = CalendarUtils.getNewRepaymentMeetingDate(recurringRule, tmpFromDate, tmpFromDate.plusDays(1), loanRepaymentInterval, frequency, workingDays, isSkipRepaymentOnFirstDayOfMonth, numberOfDays); } + if (isHolidayEnabled) { newRepaymentDate = HolidayUtil.getRepaymentRescheduleDateToIfHoliday(newRepaymentDate, holidays); } @@ -4469,12 +4132,9 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { loanRepaymentScheduleInstallment.updateDueDate(newRepaymentDate); // reset from date to get actual daysInPeriod - if (!isFirstTime) { - loanRepaymentScheduleInstallment.updateFromDate(tmpFromDate); - } + loanRepaymentScheduleInstallment.updateFromDate(tmpFromDate); - tmpFromDate = newRepaymentDate;// update with new repayment - // date + tmpFromDate = newRepaymentDate;// update with new repayment date } else { tmpFromDate = oldDueDate; } @@ -4524,8 +4184,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { loanRepaymentScheduleInstallment.updateDueDate(newRepaymentDate); // reset from date to get actual daysInPeriod loanRepaymentScheduleInstallment.updateFromDate(tmpFromDate); - tmpFromDate = newRepaymentDate;// update with new repayment - // date + tmpFromDate = newRepaymentDate;// update with new repayment date } else { tmpFromDate = oldDueDate; } @@ -4538,27 +4197,16 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { private LocalDate getMaxDateLimitForNewRepayment(final PeriodFrequencyType periodFrequencyType, final Integer loanRepaymentInterval, final LocalDate startDate) { LocalDate dueRepaymentPeriodDate = startDate; - final Integer repaidEvery = 2 * loanRepaymentInterval; + final int repaidEvery = 2 * loanRepaymentInterval; switch (periodFrequencyType) { - case DAYS: - dueRepaymentPeriodDate = startDate.plusDays(repaidEvery); - break; - case WEEKS: - dueRepaymentPeriodDate = startDate.plusWeeks(repaidEvery); - break; - case MONTHS: - dueRepaymentPeriodDate = startDate.plusMonths(repaidEvery); - break; - case YEARS: - dueRepaymentPeriodDate = startDate.plusYears(repaidEvery); - break; - case INVALID: - break; - case WHOLE_TERM: - break; + case DAYS -> dueRepaymentPeriodDate = startDate.plusDays(repaidEvery); + case WEEKS -> dueRepaymentPeriodDate = startDate.plusWeeks(repaidEvery); + case MONTHS -> dueRepaymentPeriodDate = startDate.plusMonths(repaidEvery); + case YEARS -> dueRepaymentPeriodDate = startDate.plusYears(repaidEvery); + case INVALID, WHOLE_TERM -> { + } } - return dueRepaymentPeriodDate.minusDays(1);// get 2n-1 range date from - // startDate + return dueRepaymentPeriodDate.minusDays(1);// get 2n-1 range date from startDate } private void validateDisbursementDateIsOnNonWorkingDay(final WorkingDays workingDays, final boolean allowTransactionsOnNonWorkingDay) { @@ -4630,19 +4278,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { this.loanRepaymentScheduleDetail.setInterestPeriodFrequencyType(this.loanProduct.getInterestPeriodFrequencyType()); } - public Integer getTermFrequency() { - return this.termFrequency; - } - - public Integer getTermPeriodFrequencyType() { - return this.termPeriodFrequencyType; - } - - // This method returns copy of all transactions - public List getLoanTransactions() { - return this.loanTransactions; - } - public void addLoanTransaction(final LoanTransaction loanTransaction) { this.loanTransactions.add(loanTransaction); } @@ -4665,68 +4300,68 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { String action = null; String postfix = null; switch (event) { - case LOAN_APPROVED: + case LOAN_APPROVED -> { errorMessage = "The date on which a loan is approved cannot be earlier than client's transfer date to this office"; action = "approval"; postfix = "cannot.be.before.client.transfer.date"; - break; - case LOAN_APPROVAL_UNDO: + } + case LOAN_APPROVAL_UNDO -> { errorMessage = "The date on which a loan is approved cannot be earlier than client's transfer date to this office"; action = "approval"; postfix = "cannot.be.undone.before.client.transfer.date"; - break; - case LOAN_DISBURSED: + } + case LOAN_DISBURSED -> { errorMessage = "The date on which a loan is disbursed cannot be earlier than client's transfer date to this office"; action = "disbursal"; postfix = "cannot.be.before.client.transfer.date"; - break; - case LOAN_DISBURSAL_UNDO: + } + case LOAN_DISBURSAL_UNDO -> { errorMessage = "Cannot undo a disbursal done in another branch"; action = "disbursal"; postfix = "cannot.be.undone.before.client.transfer.date"; - break; - case LOAN_REPAYMENT_OR_WAIVER: + } + case LOAN_REPAYMENT_OR_WAIVER -> { errorMessage = "The date on which a repayment or waiver is made cannot be earlier than client's transfer date to this office"; action = "repayment.or.waiver"; postfix = "cannot.be.made.before.client.transfer.date"; - break; - case LOAN_REJECTED: + } + case LOAN_REJECTED -> { errorMessage = "The date on which a loan is rejected cannot be earlier than client's transfer date to this office"; action = "reject"; postfix = "cannot.be.before.client.transfer.date"; - break; - case LOAN_WITHDRAWN: + } + case LOAN_WITHDRAWN -> { errorMessage = "The date on which a loan is withdrawn cannot be earlier than client's transfer date to this office"; action = "withdraw"; postfix = "cannot.be.before.client.transfer.date"; - break; - case WRITE_OFF_OUTSTANDING: + } + case WRITE_OFF_OUTSTANDING -> { errorMessage = "The date on which a write off is made cannot be earlier than client's transfer date to this office"; action = "writeoff"; postfix = "cannot.be.undone.before.client.transfer.date"; - break; - case REPAID_IN_FULL: + } + case REPAID_IN_FULL -> { errorMessage = "The date on which the loan is repaid in full cannot be earlier than client's transfer date to this office"; action = "close"; postfix = "cannot.be.undone.before.client.transfer.date"; - break; - case LOAN_CHARGE_PAYMENT: + } + case LOAN_CHARGE_PAYMENT -> { errorMessage = "The date on which a charge payment is made cannot be earlier than client's transfer date to this office"; action = "charge.payment"; postfix = "cannot.be.made.before.client.transfer.date"; - break; - case LOAN_REFUND: + } + case LOAN_REFUND -> { errorMessage = "The date on which a refund is made cannot be earlier than client's transfer date to this office"; action = "refund"; postfix = "cannot.be.made.before.client.transfer.date"; - break; - case LOAN_DISBURSAL_UNDO_LAST: + } + case LOAN_DISBURSAL_UNDO_LAST -> { errorMessage = "Cannot undo a last disbursal in another branch"; action = "disbursal"; postfix = "cannot.be.undone.before.client.transfer.date"; - break; - default: - break; + } + default -> { + } } throw new InvalidLoanStateTransitionException(action, postfix, errorMessage, clientOfficeJoiningDate); } @@ -4743,23 +4378,23 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { String action = null; String postfix = null; switch (event) { - case LOAN_REPAYMENT_OR_WAIVER: + case LOAN_REPAYMENT_OR_WAIVER -> { errorMessage = "The date on which a repayment or waiver is made cannot be earlier than last transaction date"; action = "repayment.or.waiver"; postfix = "cannot.be.made.before.last.transaction.date"; - break; - case WRITE_OFF_OUTSTANDING: + } + case WRITE_OFF_OUTSTANDING -> { errorMessage = "The date on which a write off is made cannot be earlier than last transaction date"; action = "writeoff"; postfix = "cannot.be.made.before.last.transaction.date"; - break; - case LOAN_CHARGE_PAYMENT: + } + case LOAN_CHARGE_PAYMENT -> { errorMessage = "The date on which a charge payment is made cannot be earlier than last transaction date"; action = "charge.payment"; postfix = "cannot.be.made.before.last.transaction.date"; - break; - default: - break; + } + default -> { + } } throw new InvalidLoanStateTransitionException(action, postfix, errorMessage, lastTransactionDate); } @@ -4817,34 +4452,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { .orElse(null); } - public LocalDate getLastUserTransactionForChargeCalc() { - LocalDate lastTransaction = getDisbursementDate(); - if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) { - lastTransaction = getLastUserTransactionDate(); - } - return lastTransaction; - } - public Set getActiveCharges() { - Set loanCharges = new HashSet<>(); - if (this.charges != null) { - for (LoanCharge charge : this.charges) { - if (charge.isActive()) { - loanCharges.add(charge); - } - } - } - return loanCharges; - } - - public Set trancheCharges() { - Set loanCharges = new HashSet<>(); - if (this.trancheCharges != null) { - for (LoanTrancheCharge charge : this.trancheCharges) { - loanCharges.add(charge); - } - } - return loanCharges; + return this.charges == null ? new HashSet<>() : this.charges.stream().filter(LoanCharge::isActive).collect(Collectors.toSet()); } public List generateInstallmentLoanCharges(final LoanCharge loanCharge) { @@ -4871,27 +4480,26 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } public void validateAccountStatus(final LoanEvent event) { - final List dataValidationErrors = new ArrayList<>(); switch (event) { - case LOAN_APPROVED: + case LOAN_APPROVED -> { if (!isSubmittedAndPendingApproval()) { final String defaultUserMessage = "Loan Account Approval is not allowed. Loan Account is not in submitted and pending approval state."; final ApiParameterError error = ApiParameterError .generalError("error.msg.loan.approve.account.is.not.submitted.and.pending.state", defaultUserMessage); dataValidationErrors.add(error); } - break; - case LOAN_APPROVAL_UNDO: + } + case LOAN_APPROVAL_UNDO -> { if (!isApproved()) { final String defaultUserMessage = "Loan Account Undo Approval is not allowed. Loan Account is not in approved state."; final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.undo.approval.account.is.not.approved", defaultUserMessage); dataValidationErrors.add(error); } - break; - case LOAN_DISBURSED: + } + case LOAN_DISBURSED -> { if ((!(isApproved() && isNotDisbursed()) && !this.loanProduct.isMultiDisburseLoan()) || (this.loanProduct.isMultiDisburseLoan() && !isAllTranchesNotDisbursed())) { final String defaultUserMessage = "Loan Disbursal is not allowed. Loan Account is not in approved and not disbursed state."; @@ -4899,8 +4507,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { .generalError("error.msg.loan.disbursal.account.is.not.approve.not.disbursed.state", defaultUserMessage); dataValidationErrors.add(error); } - break; - case LOAN_DISBURSAL_UNDO: + } + case LOAN_DISBURSAL_UNDO -> { if (!isOpen()) { final String defaultUserMessage = "Loan Undo disbursal is not allowed. Loan Account is not active."; final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.undo.disbursal.account.is.not.active", @@ -4913,121 +4521,121 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { .generalError("error.msg.loan.undo.disbursal.not.allowed.on.topup.loan", defaultUserMessage); dataValidationErrors.add(error); } - break; - case LOAN_REPAYMENT_OR_WAIVER: + } + case LOAN_REPAYMENT_OR_WAIVER -> { if (!isOpen()) { final String defaultUserMessage = "Loan Repayment (or its types) or Waiver is not allowed. Loan Account is not active."; final ApiParameterError error = ApiParameterError .generalError("error.msg.loan.repayment.or.waiver.account.is.not.active", defaultUserMessage); dataValidationErrors.add(error); } - break; - case LOAN_REJECTED: + } + case LOAN_REJECTED -> { if (!isSubmittedAndPendingApproval()) { final String defaultUserMessage = "Loan application cannot be rejected. Loan Account is not in Submitted and Pending approval state."; final ApiParameterError error = ApiParameterError .generalError("error.msg.loan.reject.account.is.not.submitted.pending.approval.state", defaultUserMessage); dataValidationErrors.add(error); } - break; - case LOAN_WITHDRAWN: + } + case LOAN_WITHDRAWN -> { if (!isSubmittedAndPendingApproval()) { final String defaultUserMessage = "Loan application cannot be withdrawn. Loan Account is not in Submitted and Pending approval state."; final ApiParameterError error = ApiParameterError .generalError("error.msg.loan.withdrawn.account.is.not.submitted.pending.approval.state", defaultUserMessage); dataValidationErrors.add(error); } - break; - case WRITE_OFF_OUTSTANDING: + } + case WRITE_OFF_OUTSTANDING -> { if (!isOpen()) { final String defaultUserMessage = "Loan Written off is not allowed. Loan Account is not active."; final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.writtenoff.account.is.not.active", defaultUserMessage); dataValidationErrors.add(error); } - break; - case WRITE_OFF_OUTSTANDING_UNDO: + } + case WRITE_OFF_OUTSTANDING_UNDO -> { if (!isClosedWrittenOff()) { final String defaultUserMessage = "Loan Undo Written off is not allowed. Loan Account is not Written off."; final ApiParameterError error = ApiParameterError .generalError("error.msg.loan.undo.writtenoff.account.is.not.written.off", defaultUserMessage); dataValidationErrors.add(error); } - break; - case LOAN_CHARGE_PAYMENT: + } + case LOAN_CHARGE_PAYMENT -> { if (!isOpen()) { final String defaultUserMessage = "Charge payment is not allowed. Loan Account is not Active."; final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.charge.payment.account.is.not.active", defaultUserMessage); dataValidationErrors.add(error); } - break; - case LOAN_CLOSED: + } + case LOAN_CLOSED -> { if (!isOpen()) { final String defaultUserMessage = "Closing Loan Account is not allowed. Loan Account is not Active."; final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.close.account.is.not.active", defaultUserMessage); dataValidationErrors.add(error); } - break; - case LOAN_EDIT_MULTI_DISBURSE_DATE: + } + case LOAN_EDIT_MULTI_DISBURSE_DATE -> { if (isClosed()) { final String defaultUserMessage = "Edit disbursement is not allowed. Loan Account is not active."; final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.edit.disbursement.account.is.not.active", defaultUserMessage); dataValidationErrors.add(error); } - break; - case LOAN_RECOVERY_PAYMENT: + } + case LOAN_RECOVERY_PAYMENT -> { if (!isClosedWrittenOff()) { final String defaultUserMessage = "Recovery repayments may only be made on loans which are written off"; final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.account.is.not.written.off", defaultUserMessage); dataValidationErrors.add(error); } - break; - case LOAN_REFUND: + } + case LOAN_REFUND -> { if (!isOpen()) { final String defaultUserMessage = "Loan Refund is not allowed. Loan Account is not active."; final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.refund.account.is.not.active", defaultUserMessage); dataValidationErrors.add(error); } - break; - case LOAN_DISBURSAL_UNDO_LAST: + } + case LOAN_DISBURSAL_UNDO_LAST -> { if (!isOpen()) { final String defaultUserMessage = "Loan Undo last disbursal is not allowed. Loan Account is not active."; final ApiParameterError error = ApiParameterError .generalError("error.msg.loan.undo.last.disbursal.account.is.not.active", defaultUserMessage); dataValidationErrors.add(error); } - break; - case LOAN_FORECLOSURE: + } + case LOAN_FORECLOSURE -> { if (!isOpen()) { final String defaultUserMessage = "Loan foreclosure is not allowed. Loan Account is not active."; final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.foreclosure.account.is.not.active", defaultUserMessage); dataValidationErrors.add(error); } - break; - case LOAN_CREDIT_BALANCE_REFUND: + } + case LOAN_CREDIT_BALANCE_REFUND -> { if (!getStatus().isOverpaid()) { final String defaultUserMessage = "Loan Credit Balance Refund is not allowed. Loan Account is not Overpaid."; final ApiParameterError error = ApiParameterError .generalError("error.msg.loan.credit.balance.refund.account.is.not.overpaid", defaultUserMessage); dataValidationErrors.add(error); } - break; - case LOAN_CHARGE_ADJUSTMENT: + } + case LOAN_CHARGE_ADJUSTMENT -> { if (!(getStatus().isActive() || getStatus().isClosedObligationsMet() || getStatus().isOverpaid())) { final String defaultUserMessage = "Loan Charge Adjustment is not allowed. Loan Account must be either Active, Fully repaid or Overpaid."; final ApiParameterError error = ApiParameterError .generalError("error.msg.loan.charge.adjustment.account.is.not.in.valid.state", defaultUserMessage); dataValidationErrors.add(error); } - break; - default: - break; + } + default -> { + } } if (!dataValidationErrors.isEmpty()) { @@ -5168,35 +4776,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return installment; } - public BigDecimal getApprovedPrincipal() { - return this.approvedPrincipal; - } - - public BigDecimal getNetDisbursalAmount() { - return netDisbursalAmount; - } - - public BigDecimal deductFromNetDisbursalAmount(final BigDecimal subtrahend) { - this.netDisbursalAmount = this.netDisbursalAmount.subtract(subtrahend); - return netDisbursalAmount; - } - - public void setNetDisbursalAmount(BigDecimal netDisbursalAmount) { - this.netDisbursalAmount = netDisbursalAmount; - } - - public BigDecimal getTotalOverpaid() { - return this.totalOverpaid; - } - public Money getTotalOverpaidAsMoney() { return Money.of(this.repaymentScheduleDetail().getCurrency(), this.totalOverpaid); } - public LocalDate getOverpaidOnDate() { - return this.overpaidOnDate; - } - public void updateIsInterestRecalculationEnabled() { this.loanRepaymentScheduleDetail.setInterestRecalculationEnabled(isInterestRecalculationEnabledForProduct()); } @@ -5216,10 +4799,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return BigDecimal.ZERO.compareTo(getLoanRepaymentScheduleDetail().getAnnualNominalInterestRate()) < 0; } - public LocalDate getExpectedMaturityDate() { - return this.expectedMaturityDate; - } - public LocalDate getMaturityDate() { return this.actualMaturityDate; } @@ -5270,7 +4849,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { mapEntry.getValue().updateLoan(this); } - /*** + /* * Commented since throwing exception if external id present for one of the transactions. for this need to save * the reversed transactions first and then new transactions. */ @@ -5314,7 +4893,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { processIncomeTransactions(); } - private void updateLoanChargesPaidBy(LoanTransaction accrual, HashMap feeDetails, + private void updateLoanChargesPaidBy(LoanTransaction accrual, Map feeDetails, LoanRepaymentScheduleInstallment installment) { @SuppressWarnings("unchecked") List loanCharges = (List) feeDetails.get("loanCharges"); @@ -5430,7 +5009,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { updateLoanOutstandingBalances(); } - private void determineFeeDetails(LocalDate fromDate, LocalDate toDate, HashMap feeDetails) { + private void determineFeeDetails(LocalDate fromDate, LocalDate toDate, Map feeDetails) { BigDecimal fee = BigDecimal.ZERO; BigDecimal penalties = BigDecimal.ZERO; @@ -5541,10 +5120,9 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } public LoanRepaymentScheduleInstallment fetchPrepaymentDetail(final ScheduleGeneratorDTO scheduleGeneratorDTO, final LocalDate onDate) { - LoanRepaymentScheduleInstallment installment = null; + LoanRepaymentScheduleInstallment installment; if (this.loanRepaymentScheduleDetail.isInterestRecalculationEnabled()) { - final MathContext mc = MoneyHelper.getMathContext(); final InterestMethod interestMethod = this.loanRepaymentScheduleDetail.getInterestMethod(); @@ -5651,18 +5229,14 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { totalInterest.getAmount(), feeCharges.getAmount(), penaltyCharges.getAmount(), false, compoundingDetails); } - public LocalDate getAccruedTill() { - return this.accruedTill; - } - public LocalDate fetchInterestRecalculateFromDate() { - LocalDate interestRecalculatedOn = null; + LocalDate recalculatedOn; if (this.interestRecalculatedOn == null) { - interestRecalculatedOn = getDisbursementDate(); + recalculatedOn = getDisbursementDate(); } else { - interestRecalculatedOn = this.interestRecalculatedOn; + recalculatedOn = this.interestRecalculatedOn; } - return interestRecalculatedOn; + return recalculatedOn; } private void updateLoanOutstandingBalances() { @@ -5678,8 +5252,9 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { if (!loanTransaction.getOverPaymentPortion(getCurrency()).isZero()) { // in case of advanced payment strategy and creditAllocations the full amount is recognized first if (this.getCreditAllocationRules() != null && this.getCreditAllocationRules().size() > 0) { - Money payedPrincipal = loanTransaction.getLoanTransactionToRepaymentScheduleMappings().stream() - .map(mapping -> mapping.getPrincipalPortion(getCurrency())).reduce(Money.zero(getCurrency()), Money::plus); + Money payedPrincipal = loanTransaction.getLoanTransactionToRepaymentScheduleMappings().stream() // + .map(mapping -> mapping.getPrincipalPortion(getCurrency())) // + .reduce(Money.zero(getCurrency()), Money::plus); transactionOutstanding = loanTransaction.getPrincipalPortion(getCurrency()).minus(payedPrincipal); } else { // in case legacy payment strategy @@ -5713,13 +5288,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return this.isNpa; } - /** - * @return List of loan repayments schedule objects - **/ - public List getRepaymentScheduleInstallments() { - return this.repaymentScheduleInstallments; - } - public Integer getLoanRepaymentScheduleInstallmentsSize() { return this.repaymentScheduleInstallments.size(); } @@ -5729,27 +5297,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { this.repaymentScheduleInstallments.add(installment); } - /** - * @return Loan product minimum repayments schedule related detail - **/ - public LoanProductRelatedDetail getLoanRepaymentScheduleDetail() { - return this.loanRepaymentScheduleDetail; - } - - /** - * @return Loan Fixed Emi amount - **/ - public BigDecimal getFixedEmiAmount() { - return this.fixedEmiAmount; - } - - /** - * @return maximum outstanding loan balance - **/ - public BigDecimal getMaxOutstandingLoanBalance() { - return this.maxOutstandingLoanBalance; - } - /** * @param dueDate * the due date of the installment @@ -5799,18 +5346,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } /** - * @param applicationCurrency - * @param restCalendarInstance - * TODO - * @param compoundingCalendarInstance - * TODO - * @param loanCalendar - * @param floatingRateDTO - * TODO - * @param isSkipRepaymentonmonthFirst - * @param numberofdays - * @param holidayDetailDTO - * Used for accessing the loan's calendar object * @return application terms of the Loan object **/ @SuppressWarnings({ "unused" }) @@ -5819,8 +5354,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { final FloatingRateDTO floatingRateDTO, final boolean isSkipRepaymentonmonthFirst, final Integer numberofdays, final HolidayDetailDTO holidayDetailDTO) { LoanProduct loanProduct = loanProduct(); - // LoanProductRelatedDetail loanProductRelatedDetail = - // getLoanRepaymentScheduleDetail(); final MonetaryCurrency currency = this.loanRepaymentScheduleDetail.getCurrency(); final Integer loanTermFrequency = getTermFrequency(); @@ -5930,10 +5463,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return this.loanRepaymentScheduleDetail; } - public void updateNumberOfRepayments(Integer numberOfRepayments) { - this.loanRepaymentScheduleDetail.setNumberOfRepayments(numberOfRepayments); - } - public void updateRescheduledOnDate(LocalDate rescheduledOnDate) { if (rescheduledOnDate != null) { @@ -5941,13 +5470,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } } - public void updateTermFrequency(Integer termFrequency) { - - if (termFrequency != null) { - this.termFrequency = termFrequency; - } - } - public boolean isFeeCompoundingEnabledForInterestRecalculation() { boolean isEnabled = false; if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) { @@ -5956,18 +5478,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return isEnabled; } - public String getAccountNumber() { - return this.accountNumber; - } - - public ExternalId getExternalId() { - return this.externalId; - } - - public Client getClient() { - return this.client; - } - public Boolean shouldCreateStandingInstructionAtDisbursement() { return this.createStandingInstructionAtDisbursement != null && this.createStandingInstructionAtDisbursement; } @@ -6037,7 +5547,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { final LoanLifecycleStateMachine loanLifecycleStateMachine, final List existingTransactionIds, final List existingReversedTransactionIds, final boolean allowTransactionsOnHoliday, final List holidays, final WorkingDays workingDays, final boolean allowTransactionsOnNonWorkingDay) { - validateAccountStatus(LoanEvent.LOAN_REFUND); validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_REFUND, loanTransaction.getTransactionDate()); @@ -6086,13 +5595,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { throw new InvalidLoanTransactionTypeException("transaction", "is.not.a.refund.transaction", errorMessage); } - final LocalDate loanTransactionDate = loanTransaction.getTransactionDate(); - if (DateUtils.isBefore(loanTransactionDate, getDisbursementDate())) { - final String errorMessage = "The transaction date cannot be before the loan disbursement date: " - + getDisbursementDate().toString(); - throw new InvalidLoanStateTransitionException("transaction", "cannot.be.before.disbursement.date", errorMessage, - loanTransactionDate, getDisbursementDate()); - } + final LocalDate loanTransactionDate = extractTransactionDate(loanTransaction); if (DateUtils.isDateInTheFuture(loanTransactionDate)) { final String errorMessage = "The transaction date cannot be in the future."; @@ -6112,7 +5615,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory .determineProcessor(this.transactionProcessingStrategyCode); - // If is a refund + // If it's a refund if (adjustedTransaction == null) { loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, new TransactionCtx(getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney()))); @@ -6135,7 +5638,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { public void handleChargebackTransaction(final LoanTransaction chargebackTransaction, final LoanLifecycleStateMachine loanLifecycleStateMachine) { - if (!chargebackTransaction.isChargeback()) { final String errorMessage = "A transaction of type chargeback was expected but not received."; throw new InvalidLoanTransactionTypeException("transaction", "is.not.a.chargeback.transaction", errorMessage); @@ -6155,7 +5657,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } public LocalDate possibleNextRefundDate() { - final LocalDate now = DateUtils.getBusinessLocalDate(); LocalDate lastTransactionDate = null; @@ -6220,16 +5721,11 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { final LoanDisbursementDetails disbursementDetail = loan.getDisbursementDetails(lastTransactionDate, lastDisbursalTransaction.getAmount()); updateLoanToLastDisbursalState(disbursementDetail); - for (Iterator iterator = this.loanTermVariations.iterator(); iterator.hasNext();) { - LoanTermVariations loanTermVariations = iterator.next(); - if ((loanTermVariations.getTermType().isDueDateVariation() - && DateUtils.isAfter(loanTermVariations.fetchDateValue(), lastTransactionDate)) - || (loanTermVariations.getTermType().isEMIAmountVariation() - && DateUtils.isEqual(loanTermVariations.getTermApplicableFrom(), lastTransactionDate)) - || DateUtils.isAfter(loanTermVariations.getTermApplicableFrom(), lastTransactionDate)) { - iterator.remove(); - } - } + this.loanTermVariations.removeIf(loanTermVariations -> (loanTermVariations.getTermType().isDueDateVariation() + && DateUtils.isAfter(loanTermVariations.fetchDateValue(), lastTransactionDate)) + || (loanTermVariations.getTermType().isEMIAmountVariation() + && DateUtils.isEqual(loanTermVariations.getTermApplicableFrom(), lastTransactionDate)) + || DateUtils.isAfter(loanTermVariations.getTermApplicableFrom(), lastTransactionDate)); reverseExistingTransactionsTillLastDisbursal(lastDisbursalTransaction); loan.recalculateScheduleFromLastTransaction(scheduleGeneratorDTO); actualChanges.put("undolastdisbursal", "true"); @@ -6243,9 +5739,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { /** * Reverse only disbursement, accruals, and repayments at disbursal transactions - * - * @param lastDisbursalTransaction - * @return */ public void reverseExistingTransactionsTillLastDisbursal(LoanTransaction lastDisbursalTransaction) { for (final LoanTransaction transaction : this.loanTransactions) { @@ -6288,26 +5781,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { updateLoanSummaryDerivedFields(); } - public Boolean getIsFloatingInterestRate() { - return this.isFloatingInterestRate; - } - - public BigDecimal getInterestRateDifferential() { - return this.interestRateDifferential; - } - - public void setIsFloatingInterestRate(Boolean isFloatingInterestRate) { - this.isFloatingInterestRate = isFloatingInterestRate; - } - - public void setInterestRateDifferential(BigDecimal interestRateDifferential) { - this.interestRateDifferential = interestRateDifferential; - } - - public List getLoanTermVariations() { - return this.loanTermVariations; - } - private int adjustNumberOfRepayments() { int repaymetsForAdjust = 0; for (LoanTermVariations loanTermVariations : this.loanTermVariations) { @@ -6334,10 +5807,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return this.repaymentScheduleDetail().getNumberOfRepayments() + adjustNumberOfRepayments(); } - public void setExpectedFirstRepaymentOnDate(LocalDate expectedFirstRepaymentOnDate) { - this.expectedFirstRepaymentOnDate = expectedFirstRepaymentOnDate; - } - /* * get the next repayment LocalDate for rescheduling at the time of disbursement */ @@ -6365,10 +5834,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { if (isMultiDisburmentLoan() && loanCharge.getCharge().getChargeTimeType().equals(ChargeTimeType.DISBURSEMENT.getValue())) { amount = getApprovedPrincipal(); } else { - // If charge type is specified due date and loan is multi disburment - // loan. - // Then we need to get as of this loan charge due date how much - // amount disbursed. + // If charge type is specified due date and loan is multi disburment loan. + // Then we need to get as of this loan charge due date how much amount disbursed. if (loanCharge.isSpecifiedDueDate() && this.isMultiDisburmentLoan()) { for (final LoanDisbursementDetails loanDisbursementDetails : this.getDisbursementDetails()) { if (!DateUtils.isAfter(loanDisbursementDetails.expectedDisbursementDate(), loanCharge.getDueDate())) { @@ -6382,26 +5849,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return amount; } - public void updatePostDatedChecks(final List postDatedChecks) { - this.postDatedChecks = postDatedChecks; - } - - public List getPostDatedChecks() { - return this.postDatedChecks; - } - public void updateWriteOffReason(CodeValue writeOffReason) { this.writeOffReason = writeOffReason; } - public Group getGroup() { - return group; - } - - public LoanProduct getLoanProduct() { - return loanProduct; - } - public LoanRepaymentScheduleInstallment fetchLoanForeclosureDetail(final LocalDate closureDate) { Money[] receivables = retriveIncomeOutstandingTillDate(closureDate); Money totalPrincipal = Money.of(getCurrency(), this.getLoanSummary().getTotalPrincipalOutstanding()); @@ -6595,7 +6046,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { validateAccountStatus(event); validateForForeclosure(repaymentTransaction.getTransactionDate()); this.loanSubStatus = LoanSubStatus.FORECLOSED.getValue(); - applyAccurals(); + applyAccruals(); return handleRepaymentOrRecoveryOrWaiverTransaction(repaymentTransaction, loanLifecycleStateMachine, null, scheduleGeneratorDTO); } @@ -6695,10 +6146,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } } - public Integer getLoanSubStatus() { - return this.loanSubStatus; - } - private boolean isForeclosure() { boolean isForeClosure = false; if (this.loanSubStatus != null) { @@ -6720,10 +6167,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return !retData.isEmpty() ? retData : null; } - public void setIsTopup(final boolean isTopup) { - this.isTopup = isTopup; - } - public boolean isTopup() { return this.isTopup; } @@ -6732,10 +6175,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { this.fraud = value; } - public boolean isFraud() { - return this.fraud; - } - public BigDecimal getFirstDisbursalAmount() { BigDecimal firstDisbursalAmount; @@ -6799,26 +6238,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return AccountType.fromInt(this.loanType).isIndividualAccount(); } - public void setRates(List rates) { - this.rates = rates; - } - - public List getRates() { - return rates; - } - public AccountType getLoanType() { return AccountType.fromInt(loanType); } - public void setLoanType(Integer loanType) { - this.loanType = loanType; - } - - public Set getLoanCollateralManagements() { - return this.loanCollateralManagements; - } - public void adjustNetDisbursalAmount(BigDecimal adjustedAmount) { this.netDisbursalAmount = adjustedAmount.subtract(this.deriveSumTotalOfChargesDueAtDisbursement()); } @@ -6839,27 +6262,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return (getLoanProduct().getDelinquencyBucket() != null); } - public Long getAgeOfOverdueDays(LocalDate baseDate) { - Long ageOfOverdueDays = 0L; - - List installments = getRepaymentScheduleInstallments(); - for (final LoanRepaymentScheduleInstallment installment : installments) { - if (!installment.isObligationsMet()) { - ageOfOverdueDays = DateUtils.getDifferenceInDays(installment.getDueDate(), baseDate); - break; - } - } - return ageOfOverdueDays; - } - - public LocalDate getLastClosedBusinessDate() { - return this.lastClosedBusinessDate; - } - - public void setLastClosedBusinessDate(LocalDate lastClosedBusinessDate) { - this.lastClosedBusinessDate = lastClosedBusinessDate; - } - public void markAsChargedOff(final LocalDate chargedOffOn, final AppUser chargedOffBy, final CodeValue chargeOffReason) { this.chargedOff = true; this.chargedOffBy = chargedOffBy; @@ -6874,10 +6276,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { this.chargeOffReason = null; } - public boolean isChargedOff() { - return this.chargedOff; - } - public LoanRepaymentScheduleInstallment getLastLoanRepaymentScheduleInstallment() { return getRepaymentScheduleInstallments().get(getRepaymentScheduleInstallments().size() - 1); } @@ -6908,50 +6306,15 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { .orElse(null); } - public LocalDate getChargedOffOnDate() { - return chargedOffOnDate; - } - - public LoanInterestRecalculationDetails getLoanInterestRecalculationDetails() { - return loanInterestRecalculationDetails; - } - - public List getPaymentAllocationRules() { - return paymentAllocationRules; - } - - public LoanPaymentAllocationRule getPaymentAllocationRuleOrDefault(PaymentAllocationTransactionType transactionType) { - Optional paymentAllocationRule = this.getPaymentAllocationRules().stream() - .filter(rule -> rule.getTransactionType().equals(transactionType)).findFirst(); - return paymentAllocationRule.orElse(this.getPaymentAllocationRules().stream() - .filter(rule -> rule.getTransactionType().equals(PaymentAllocationTransactionType.DEFAULT)).findFirst().get()); - } - - public void setPaymentAllocationRules(List loanPaymentAllocationRules) { - this.paymentAllocationRules = loanPaymentAllocationRules; - } - - public List getCreditAllocationRules() { - return creditAllocationRules; - } - - public void setCreditAllocationRules(List loanCreditAllocationRules) { - this.creditAllocationRules = loanCreditAllocationRules; - } - - public String getTransactionProcessingStrategyCode() { - return transactionProcessingStrategyCode; - } - - public String getTransactionProcessingStrategyName() { - return transactionProcessingStrategyName; - } - - public boolean isEnableInstallmentLevelDelinquency() { - return this.enableInstallmentLevelDelinquency; - } - public void updateEnableInstallmentLevelDelinquency(boolean enableInstallmentLevelDelinquency) { this.enableInstallmentLevelDelinquency = enableInstallmentLevelDelinquency; } + + public void deductFromNetDisbursalAmount(final BigDecimal subtrahend) { + this.netDisbursalAmount = this.netDisbursalAmount.subtract(subtrahend); + } + + public void setIsTopup(boolean topup) { + isTopup = topup; + } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummary.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummary.java index 271f88f51..1956682eb 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummary.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummary.java @@ -140,103 +140,39 @@ public class LoanSummary { this.totalFeeChargesDueAtDisbursement = totalFeeChargesDueAtDisbursement; } - public void updateTotalFeeChargesDueAtDisbursement(final BigDecimal totalFeeChargesDueAtDisbursement) { - this.totalFeeChargesDueAtDisbursement = totalFeeChargesDueAtDisbursement; - } - - public Money getTotalFeeChargesDueAtDisbursement(final MonetaryCurrency currency) { - return Money.of(currency, this.totalFeeChargesDueAtDisbursement); - } - - public Money getTotalOutstanding(final MonetaryCurrency currency) { - return Money.of(currency, this.totalOutstanding); - } - - public void updateFeeChargeOutstanding(final BigDecimal totalFeeChargesOutstanding) { - this.totalFeeChargesOutstanding = totalFeeChargesOutstanding; - } - - public void updatePenaltyChargeOutstanding(final BigDecimal totalPenaltyChargesOutstanding) { - this.totalPenaltyChargesOutstanding = totalPenaltyChargesOutstanding; - } - - public void updateFeeChargesWaived(final BigDecimal totalFeeChargesWaived) { - this.totalFeeChargesWaived = totalFeeChargesWaived; - } - - public void updatePenaltyChargesWaived(final BigDecimal totalPenaltyChargesWaived) { - this.totalPenaltyChargesWaived = totalPenaltyChargesWaived; - } - - public boolean isRepaidInFull(final MonetaryCurrency currency) { - return getTotalOutstanding(currency).isZero(); - } - - public BigDecimal getTotalInterestCharged() { - return this.totalInterestCharged; - } - - public BigDecimal getTotalPrincipalOutstanding() { - return this.totalPrincipalOutstanding; - } - - public BigDecimal getTotalInterestOutstanding() { - return this.totalInterestOutstanding; - } - - public BigDecimal getTotalFeeChargesOutstanding() { - return this.totalFeeChargesOutstanding; - } - - public BigDecimal getTotalPenaltyChargesOutstanding() { - return this.totalPenaltyChargesOutstanding; - } - - public BigDecimal getTotalOutstanding() { - return this.totalOutstanding; - } - - public void updateTotalOutstanding(final BigDecimal newTotalOutstanding) { - this.totalOutstanding = newTotalOutstanding; - } - - public void updateTotalWaived(final BigDecimal totalWaived) { - this.totalWaived = totalWaived; - } - /** * All fields but totalFeeChargesDueAtDisbursement should be reset. */ public void zeroFields() { - this.totalPrincipalDisbursed = BigDecimal.ZERO; - this.totalPrincipalAdjustments = BigDecimal.ZERO; + this.totalCostOfLoan = BigDecimal.ZERO; + this.totalExpectedCostOfLoan = BigDecimal.ZERO; + this.totalExpectedRepayment = BigDecimal.ZERO; this.totalFeeAdjustments = BigDecimal.ZERO; - this.totalPenaltyAdjustments = BigDecimal.ZERO; - this.totalPrincipalRepaid = BigDecimal.ZERO; - this.totalPrincipalWrittenOff = BigDecimal.ZERO; - this.totalPrincipalOutstanding = BigDecimal.ZERO; - this.totalInterestCharged = BigDecimal.ZERO; - this.totalInterestRepaid = BigDecimal.ZERO; - this.totalInterestWaived = BigDecimal.ZERO; - this.totalInterestWrittenOff = BigDecimal.ZERO; - this.totalInterestOutstanding = BigDecimal.ZERO; this.totalFeeChargesCharged = BigDecimal.ZERO; + this.totalFeeChargesOutstanding = BigDecimal.ZERO; this.totalFeeChargesRepaid = BigDecimal.ZERO; this.totalFeeChargesWaived = BigDecimal.ZERO; this.totalFeeChargesWrittenOff = BigDecimal.ZERO; - this.totalFeeChargesOutstanding = BigDecimal.ZERO; + this.totalInterestCharged = BigDecimal.ZERO; + this.totalInterestOutstanding = BigDecimal.ZERO; + this.totalInterestRepaid = BigDecimal.ZERO; + this.totalInterestWaived = BigDecimal.ZERO; + this.totalInterestWrittenOff = BigDecimal.ZERO; + this.totalOutstanding = BigDecimal.ZERO; + this.totalPenaltyAdjustments = BigDecimal.ZERO; this.totalPenaltyChargesCharged = BigDecimal.ZERO; + this.totalPenaltyChargesOutstanding = BigDecimal.ZERO; this.totalPenaltyChargesRepaid = BigDecimal.ZERO; this.totalPenaltyChargesWaived = BigDecimal.ZERO; this.totalPenaltyChargesWrittenOff = BigDecimal.ZERO; - this.totalPenaltyChargesOutstanding = BigDecimal.ZERO; - this.totalExpectedRepayment = BigDecimal.ZERO; + this.totalPrincipalAdjustments = BigDecimal.ZERO; + this.totalPrincipalDisbursed = BigDecimal.ZERO; + this.totalPrincipalOutstanding = BigDecimal.ZERO; + this.totalPrincipalRepaid = BigDecimal.ZERO; + this.totalPrincipalWrittenOff = BigDecimal.ZERO; this.totalRepayment = BigDecimal.ZERO; - this.totalExpectedCostOfLoan = BigDecimal.ZERO; - this.totalCostOfLoan = BigDecimal.ZERO; this.totalWaived = BigDecimal.ZERO; this.totalWrittenOff = BigDecimal.ZERO; - this.totalOutstanding = BigDecimal.ZERO; } public void updateSummary(final MonetaryCurrency currency, final Money principal, @@ -327,58 +263,43 @@ public class LoanSummary { this.totalOutstanding = totalOutstanding.getAmount(); } - public BigDecimal getTotalPrincipalDisbursed() { - return this.totalPrincipalDisbursed; + public void updateTotalFeeChargesDueAtDisbursement(final BigDecimal totalFeeChargesDueAtDisbursement) { + this.totalFeeChargesDueAtDisbursement = totalFeeChargesDueAtDisbursement; } - public BigDecimal getTotalPrincipalRepaid() { - return this.totalPrincipalRepaid; + public Money getTotalFeeChargesDueAtDisbursement(final MonetaryCurrency currency) { + return Money.of(currency, this.totalFeeChargesDueAtDisbursement); } - public BigDecimal getTotalWrittenOff() { - return this.totalWrittenOff; + public Money getTotalOutstanding(final MonetaryCurrency currency) { + return Money.of(currency, this.totalOutstanding); } - /** - * @return total interest repaid - **/ - public BigDecimal getTotalInterestRepaid() { - return this.totalInterestRepaid; + public void updateFeeChargeOutstanding(final BigDecimal totalFeeChargesOutstanding) { + this.totalFeeChargesOutstanding = totalFeeChargesOutstanding; } - public BigDecimal getTotalFeeChargesCharged() { - return this.totalFeeChargesCharged; + public void updatePenaltyChargeOutstanding(final BigDecimal totalPenaltyChargesOutstanding) { + this.totalPenaltyChargesOutstanding = totalPenaltyChargesOutstanding; } - public BigDecimal getTotalPenaltyChargesCharged() { - return this.totalPenaltyChargesCharged; + public void updateFeeChargesWaived(final BigDecimal totalFeeChargesWaived) { + this.totalFeeChargesWaived = totalFeeChargesWaived; } - public BigDecimal getTotalPrincipalWrittenOff() { - return this.totalPrincipalWrittenOff; + public void updatePenaltyChargesWaived(final BigDecimal totalPenaltyChargesWaived) { + this.totalPenaltyChargesWaived = totalPenaltyChargesWaived; } - public BigDecimal getTotalInterestWaived() { - return this.totalInterestWaived; + public boolean isRepaidInFull(final MonetaryCurrency currency) { + return getTotalOutstanding(currency).isZero(); } - public BigDecimal getTotalFeeChargesRepaid() { - return this.totalFeeChargesRepaid; + public void updateTotalOutstanding(final BigDecimal newTotalOutstanding) { + this.totalOutstanding = newTotalOutstanding; } - public BigDecimal getTotalFeeChargesWaived() { - return this.totalFeeChargesWaived; - } - - public BigDecimal getTotalPenaltyChargesRepaid() { - return this.totalPenaltyChargesRepaid; - } - - public BigDecimal getTotalPenaltyChargesWaived() { - return this.totalPenaltyChargesWaived; - } - - public BigDecimal getTotalExpectedRepayment() { - return this.totalExpectedRepayment; + public void updateTotalWaived(final BigDecimal totalWaived) { + this.totalWaived = totalWaived; } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java index 19358daa8..01bb58223 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java @@ -33,6 +33,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.function.Consumer; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -235,509 +236,507 @@ public final class LoanApplicationValidator { final Long clientId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.clientIdParameterName, element); final Long groupId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.groupIdParameterName, element); - Client client = null; - if (clientId != null) { - client = this.clientRepository.findOneWithNotFoundDetection(clientId); - } - Group group = null; - if (groupId != null) { - group = this.groupRepository.findOneWithNotFoundDetection(groupId); - } + final Client client = clientId != null ? this.clientRepository.findOneWithNotFoundDetection(clientId) : null; + final Group group = groupId != null ? this.groupRepository.findOneWithNotFoundDetection(groupId) : null; validateClientOrGroup(client, group, productId); - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan"); + validateOrThrow("loan", baseDataValidator -> { + final String loanTypeStr = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.loanTypeParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.loanTypeParameterName).value(loanTypeStr).notNull(); - final String loanTypeStr = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.loanTypeParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.loanTypeParameterName).value(loanTypeStr).notNull(); + if (!StringUtils.isBlank(loanTypeStr)) { + final AccountType loanType = AccountType.fromName(loanTypeStr); + baseDataValidator.reset().parameter(LoanApiConstants.loanTypeParameterName).value(loanType.getValue()).inMinMaxRange(1, 4); - if (!StringUtils.isBlank(loanTypeStr)) { - final AccountType loanType = AccountType.fromName(loanTypeStr); - baseDataValidator.reset().parameter(LoanApiConstants.loanTypeParameterName).value(loanType.getValue()).inMinMaxRange(1, 4); + if (loanType.isIndividualAccount()) { + baseDataValidator.reset().parameter(LoanApiConstants.clientIdParameterName).value(clientId).notNull() + .longGreaterThanZero(); + baseDataValidator.reset().parameter(LoanApiConstants.groupIdParameterName).value(groupId) + .mustBeBlankWhenParameterProvided(LoanApiConstants.clientIdParameterName, clientId); + } - if (loanType.isIndividualAccount()) { - baseDataValidator.reset().parameter(LoanApiConstants.clientIdParameterName).value(clientId).notNull().longGreaterThanZero(); - baseDataValidator.reset().parameter(LoanApiConstants.groupIdParameterName).value(groupId) - .mustBeBlankWhenParameterProvided(LoanApiConstants.clientIdParameterName, clientId); - } + if (loanType.isGroupAccount()) { + baseDataValidator.reset().parameter(LoanApiConstants.groupIdParameterName).value(groupId).notNull() + .longGreaterThanZero(); + baseDataValidator.reset().parameter(LoanApiConstants.clientIdParameterName).value(clientId) + .mustBeBlankWhenParameterProvided(LoanApiConstants.groupIdParameterName, groupId); + } - if (loanType.isGroupAccount()) { - baseDataValidator.reset().parameter(LoanApiConstants.groupIdParameterName).value(groupId).notNull().longGreaterThanZero(); - baseDataValidator.reset().parameter(LoanApiConstants.clientIdParameterName).value(clientId) - .mustBeBlankWhenParameterProvided(LoanApiConstants.groupIdParameterName, groupId); - } - - if (loanType.isJLGAccount()) { - baseDataValidator.reset().parameter(LoanApiConstants.clientIdParameterName).value(clientId).notNull() - .integerGreaterThanZero(); - baseDataValidator.reset().parameter(LoanApiConstants.groupIdParameterName).value(groupId).notNull().longGreaterThanZero(); - - // if it is JLG loan that must have meeting details - if (isMeetingMandatoryForJLGLoans) { - - final Long calendarId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.calendarIdParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.calendarIdParameterName).value(calendarId).notNull() + if (loanType.isJLGAccount()) { + baseDataValidator.reset().parameter(LoanApiConstants.clientIdParameterName).value(clientId).notNull() .integerGreaterThanZero(); + baseDataValidator.reset().parameter(LoanApiConstants.groupIdParameterName).value(groupId).notNull() + .longGreaterThanZero(); - // if it is JLG loan then must have a value for - // syncDisbursement passed in - final Boolean syncDisbursement = this.fromApiJsonHelper - .extractBooleanNamed(LoanApiConstants.syncDisbursementWithMeetingParameterName, element); + // if it is JLG loan that must have meeting details + if (isMeetingMandatoryForJLGLoans) { - if (syncDisbursement == null) { - baseDataValidator.reset().parameter(LoanApiConstants.syncDisbursementWithMeetingParameterName) - .value(syncDisbursement).trueOrFalseRequired(false); - } - } + final Long calendarId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.calendarIdParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.calendarIdParameterName).value(calendarId).notNull() + .integerGreaterThanZero(); - } + // if it is JLG loan then must have a value for + // syncDisbursement passed in + final Boolean syncDisbursement = this.fromApiJsonHelper + .extractBooleanNamed(LoanApiConstants.syncDisbursementWithMeetingParameterName, element); - } - - boolean isEqualAmortization = false; - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.isEqualAmortizationParam, element)) { - isEqualAmortization = this.fromApiJsonHelper.extractBooleanNamed(LoanApiConstants.isEqualAmortizationParam, element); - baseDataValidator.reset().parameter(LoanApiConstants.isEqualAmortizationParam).value(isEqualAmortization).ignoreIfNull() - .validateForBooleanValue(); - if (isEqualAmortization && loanProduct.isInterestRecalculationEnabled()) { - throw new EqualAmortizationUnsupportedFeatureException("interest.recalculation", "interest recalculation"); - } - } - - BigDecimal fixedPrincipalPercentagePerInstallment = this.fromApiJsonHelper - .extractBigDecimalWithLocaleNamed(LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName, element); - baseDataValidator.reset().parameter(LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName) - .value(fixedPrincipalPercentagePerInstallment).notLessThanMin(BigDecimal.ONE).notGreaterThanMax(BigDecimal.valueOf(100)); - - baseDataValidator.reset().parameter(LoanApiConstants.productIdParameterName).value(productId).notNull().integerGreaterThanZero(); - - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.accountNoParameterName, element)) { - final String accountNo = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.accountNoParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.accountNoParameterName).value(accountNo).ignoreIfNull() - .notExceedingLengthOf(20); - } - - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.externalIdParameterName, element)) { - final String externalId = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.externalIdParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.externalIdParameterName).value(externalId).ignoreIfNull() - .notExceedingLengthOf(100); - } - - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.fundIdParameterName, element)) { - final Long fundId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.fundIdParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.fundIdParameterName).value(fundId).ignoreIfNull().integerGreaterThanZero(); - } - - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.loanOfficerIdParameterName, element)) { - final Long loanOfficerId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.loanOfficerIdParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.loanOfficerIdParameterName).value(loanOfficerId).ignoreIfNull() - .integerGreaterThanZero(); - } - - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.loanPurposeIdParameterName, element)) { - final Long loanPurposeId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.loanPurposeIdParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.loanPurposeIdParameterName).value(loanPurposeId).ignoreIfNull() - .integerGreaterThanZero(); - } - - final BigDecimal principal = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.principalParamName, element); - baseDataValidator.reset().parameter(LoanApiConstants.principalParamName).value(principal).notNull().positiveAmount(); - - final Integer loanTermFrequency = this.fromApiJsonHelper - .extractIntegerWithLocaleNamed(LoanApiConstants.loanTermFrequencyParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.loanTermFrequencyParameterName).value(loanTermFrequency).notNull() - .integerGreaterThanZero(); - - final Integer loanTermFrequencyType = this.fromApiJsonHelper - .extractIntegerSansLocaleNamed(LoanApiConstants.loanTermFrequencyTypeParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.loanTermFrequencyTypeParameterName).value(loanTermFrequencyType).notNull() - .inMinMaxRange(0, 3); - - final Integer numberOfRepayments = this.fromApiJsonHelper - .extractIntegerWithLocaleNamed(LoanApiConstants.numberOfRepaymentsParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.numberOfRepaymentsParameterName).value(numberOfRepayments).notNull() - .integerGreaterThanZero(); - - final Integer repaymentEvery = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(LoanApiConstants.repaymentEveryParameterName, - element); - baseDataValidator.reset().parameter(LoanApiConstants.repaymentEveryParameterName).value(repaymentEvery).notNull() - .integerGreaterThanZero(); - - final Integer repaymentEveryType = this.fromApiJsonHelper - .extractIntegerSansLocaleNamed(LoanApiConstants.repaymentFrequencyTypeParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.repaymentFrequencyTypeParameterName).value(repaymentEveryType).notNull() - .inMinMaxRange(0, 3); - - CalendarUtils.validateNthDayOfMonthFrequency(baseDataValidator, LoanApiConstants.repaymentFrequencyNthDayTypeParameterName, - LoanApiConstants.repaymentFrequencyDayOfWeekTypeParameterName, element, this.fromApiJsonHelper); - - final Integer interestType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(LoanApiConstants.interestTypeParameterName, - element); - baseDataValidator.reset().parameter(LoanApiConstants.interestTypeParameterName).value(interestType).notNull().inMinMaxRange(0, 1); - - final Integer interestCalculationPeriodType = this.fromApiJsonHelper - .extractIntegerSansLocaleNamed(LoanApiConstants.interestCalculationPeriodTypeParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.interestCalculationPeriodTypeParameterName) - .value(interestCalculationPeriodType).notNull().inMinMaxRange(0, 1); - - boolean isInterestBearing = false; - if (loanProduct.isLinkedToFloatingInterestRate()) { - if (isEqualAmortization) { - throw new EqualAmortizationUnsupportedFeatureException("floating.interest.rate", "floating interest rate"); - } - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.interestRatePerPeriodParameterName, element)) { - baseDataValidator.reset().parameter(LoanApiConstants.interestRatePerPeriodParameterName).failWithCode( - "not.supported.loanproduct.linked.to.floating.rate", - "interestRatePerPeriod param is not supported, selected Loan Product is linked with floating interest rate."); - } - - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.isFloatingInterestRate, element)) { - final Boolean isFloatingInterestRate = this.fromApiJsonHelper.extractBooleanNamed(LoanApiConstants.isFloatingInterestRate, - element); - if (isFloatingInterestRate != null && isFloatingInterestRate - && !loanProduct.getFloatingRates().isFloatingInterestRateCalculationAllowed()) { - baseDataValidator.reset().parameter(LoanApiConstants.isFloatingInterestRate).failWithCode( - "true.not.supported.for.selected.loanproduct", - "isFloatingInterestRate value of true not supported for selected Loan Product."); - } - } else { - baseDataValidator.reset().parameter(LoanApiConstants.isFloatingInterestRate).trueOrFalseRequired(false); - } - - if (interestType != null && interestType.equals(InterestMethod.FLAT.getValue())) { - baseDataValidator.reset().parameter(LoanApiConstants.interestTypeParameterName).failWithCode( - "should.be.0.for.selected.loan.product", - "interestType should be DECLINING_BALANCE for selected Loan Product as it is linked to floating rates."); - } - - final String interestRateDifferentialParameterName = LoanApiConstants.interestRateDifferential; - final BigDecimal interestRateDifferential = this.fromApiJsonHelper - .extractBigDecimalWithLocaleNamed(interestRateDifferentialParameterName, element); - baseDataValidator.reset().parameter(interestRateDifferentialParameterName).value(interestRateDifferential).notNull() - .zeroOrPositiveAmount().inMinAndMaxAmountRange(loanProduct.getFloatingRates().getMinDifferentialLendingRate(), - loanProduct.getFloatingRates().getMaxDifferentialLendingRate()); - isInterestBearing = true; - } else { - - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.isFloatingInterestRate, element)) { - baseDataValidator.reset().parameter(LoanApiConstants.isFloatingInterestRate).failWithCode( - "not.supported.loanproduct.not.linked.to.floating.rate", - "isFloatingInterestRate param is not supported, selected Loan Product is not linked with floating interest rate."); - } - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.interestRateDifferential, element)) { - baseDataValidator.reset().parameter(LoanApiConstants.interestRateDifferential).failWithCode( - "not.supported.loanproduct.not.linked.to.floating.rate", - "interestRateDifferential param is not supported, selected Loan Product is not linked with floating interest rate."); - } - - final BigDecimal interestRatePerPeriod = this.fromApiJsonHelper - .extractBigDecimalWithLocaleNamed(LoanApiConstants.interestRatePerPeriodParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.interestRatePerPeriodParameterName).value(interestRatePerPeriod).notNull() - .zeroOrPositiveAmount(); - isInterestBearing = interestRatePerPeriod.compareTo(BigDecimal.ZERO) > 0; - } - - final Integer amortizationType = this.fromApiJsonHelper - .extractIntegerSansLocaleNamed(LoanApiConstants.amortizationTypeParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.amortizationTypeParameterName).value(amortizationType).notNull() - .inMinMaxRange(0, 1); - - if (!AmortizationMethod.EQUAL_PRINCIPAL.getValue().equals(amortizationType) && fixedPrincipalPercentagePerInstallment != null) { - baseDataValidator.reset().parameter(LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName).failWithCode( - "not.supported.principal.fixing.not.allowed.with.equal.installments", - "Principal fixing cannot be done with equal installment amortization"); - } - - final LocalDate expectedDisbursementDate = this.fromApiJsonHelper - .extractLocalDateNamed(LoanApiConstants.expectedDisbursementDateParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.expectedDisbursementDateParameterName).value(expectedDisbursementDate) - .notNull(); - - // grace validation - final Integer graceOnPrincipalPayment = this.fromApiJsonHelper - .extractIntegerWithLocaleNamed(LoanApiConstants.graceOnPrincipalPaymentParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.graceOnPrincipalPaymentParameterName).value(graceOnPrincipalPayment) - .zeroOrPositiveAmount(); - - final Integer graceOnInterestPayment = this.fromApiJsonHelper - .extractIntegerWithLocaleNamed(LoanApiConstants.graceOnInterestPaymentParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.graceOnInterestPaymentParameterName).value(graceOnInterestPayment) - .zeroOrPositiveAmount(); - - final Integer graceOnInterestCharged = this.fromApiJsonHelper - .extractIntegerWithLocaleNamed(LoanApiConstants.graceOnInterestChargedParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.graceOnInterestChargedParameterName).value(graceOnInterestCharged) - .zeroOrPositiveAmount(); - - final Integer graceOnArrearsAgeing = this.fromApiJsonHelper - .extractIntegerWithLocaleNamed(LoanProductConstants.GRACE_ON_ARREARS_AGEING_PARAMETER_NAME, element); - baseDataValidator.reset().parameter(LoanProductConstants.GRACE_ON_ARREARS_AGEING_PARAMETER_NAME).value(graceOnArrearsAgeing) - .zeroOrPositiveAmount(); - - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.interestChargedFromDateParameterName, element)) { - final LocalDate interestChargedFromDate = this.fromApiJsonHelper - .extractLocalDateNamed(LoanApiConstants.interestChargedFromDateParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.interestChargedFromDateParameterName).value(interestChargedFromDate) - .ignoreIfNull().notNull(); - } - - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.repaymentsStartingFromDateParameterName, element)) { - final LocalDate repaymentsStartingFromDate = this.fromApiJsonHelper - .extractLocalDateNamed(LoanApiConstants.repaymentsStartingFromDateParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.repaymentsStartingFromDateParameterName).value(repaymentsStartingFromDate) - .ignoreIfNull().notNull(); - } - - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.inArrearsToleranceParameterName, element)) { - final BigDecimal inArrearsTolerance = this.fromApiJsonHelper - .extractBigDecimalWithLocaleNamed(LoanApiConstants.inArrearsToleranceParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.inArrearsToleranceParameterName).value(inArrearsTolerance).ignoreIfNull() - .zeroOrPositiveAmount(); - } - - final LocalDate submittedOnDate = this.fromApiJsonHelper.extractLocalDateNamed(LoanApiConstants.submittedOnDateParameterName, - element); - - baseDataValidator.reset().parameter(LoanApiConstants.submittedOnDateParameterName).value(submittedOnDate).notNull(); - - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.submittedOnNoteParameterName, element)) { - final String submittedOnNote = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.submittedOnNoteParameterName, - element); - baseDataValidator.reset().parameter(LoanApiConstants.submittedOnNoteParameterName).value(submittedOnNote).ignoreIfNull() - .notExceedingLengthOf(500); - } - - final String transactionProcessingStrategy = this.fromApiJsonHelper - .extractStringNamed(LoanApiConstants.transactionProcessingStrategyCodeParameterName, element); - - validateTransactionProcessingStrategy(transactionProcessingStrategy, loanProduct, baseDataValidator); - - validateLinkedSavingsAccount(element, baseDataValidator); - - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.createStandingInstructionAtDisbursementParameterName, element)) { - final Boolean createStandingInstructionAtDisbursement = this.fromApiJsonHelper - .extractBooleanNamed(LoanApiConstants.createStandingInstructionAtDisbursementParameterName, element); - final Long linkAccountId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.linkAccountIdParameterName, element); - - if (createStandingInstructionAtDisbursement) { - baseDataValidator.reset().parameter(LoanApiConstants.linkAccountIdParameterName).value(linkAccountId).notNull() - .longGreaterThanZero(); - } - } - - // charges - loanChargeApiJsonValidator.validateLoanCharges(element, loanProduct, baseDataValidator); - - /** - * TODO: Add collaterals for other loan accounts if needed. For now it's only applicable for individual - * accounts. (loanType.isJLG() || loanType.isGLIM()) - */ - - if (!StringUtils.isBlank(loanTypeStr)) { - final AccountType loanType = AccountType.fromName(loanTypeStr); - - // collateral - if (loanType.isIndividualAccount() && element.isJsonObject() - && this.fromApiJsonHelper.parameterExists(LoanApiConstants.collateralParameterName, element)) { - final JsonObject topLevelJsonElement = element.getAsJsonObject(); - final Locale locale = this.fromApiJsonHelper.extractLocaleParameter(topLevelJsonElement); - if (topLevelJsonElement.get(LoanApiConstants.collateralParameterName).isJsonArray()) { - - final Type collateralParameterTypeOfMap = new TypeToken>() { - - }.getType(); - final Set supportedParameters = new HashSet<>( - Arrays.asList(LoanApiConstants.clientCollateralIdParameterName, LoanApiConstants.quantityParameterName)); - final JsonArray array = topLevelJsonElement.get(LoanApiConstants.collateralParameterName).getAsJsonArray(); - for (int i = 1; i <= array.size(); i++) { - final JsonObject collateralItemElement = array.get(i - 1).getAsJsonObject(); - - final String collateralJson = this.fromApiJsonHelper.toJson(collateralItemElement); - this.fromApiJsonHelper.checkForUnsupportedParameters(collateralParameterTypeOfMap, collateralJson, - supportedParameters); - - final Long clientCollateralId = this.fromApiJsonHelper - .extractLongNamed(LoanApiConstants.clientCollateralIdParameterName, collateralItemElement); - baseDataValidator.reset().parameter(LoanApiConstants.collateralParameterName) - .parameterAtIndexArray(LoanApiConstants.clientCollateralIdParameterName, i).value(clientCollateralId) - .notNull().integerGreaterThanZero(); - - final BigDecimal quantity = this.fromApiJsonHelper.extractBigDecimalNamed(LoanApiConstants.quantityParameterName, - collateralItemElement, locale); - baseDataValidator.reset().parameter(LoanApiConstants.collateralParameterName) - .parameterAtIndexArray(LoanApiConstants.quantityParameterName, i).value(quantity).notNull() - .positiveAmount(); - - final ClientCollateralManagement clientCollateralManagement = this.clientCollateralManagementRepositoryWrapper - .getCollateral(clientCollateralId); - - if (clientCollateralId != null && BigDecimal.valueOf(0).compareTo(clientCollateralManagement.getQuantity()) >= 0) { - throw new InvalidAmountOfCollateralQuantity(clientCollateralManagement.getQuantity()); + if (syncDisbursement == null) { + baseDataValidator.reset().parameter(LoanApiConstants.syncDisbursementWithMeetingParameterName) + .value(syncDisbursement).trueOrFalseRequired(false); } + } + } + } + + boolean isEqualAmortization = false; + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.isEqualAmortizationParam, element)) { + isEqualAmortization = this.fromApiJsonHelper.extractBooleanNamed(LoanApiConstants.isEqualAmortizationParam, element); + baseDataValidator.reset().parameter(LoanApiConstants.isEqualAmortizationParam).value(isEqualAmortization).ignoreIfNull() + .validateForBooleanValue(); + if (isEqualAmortization && loanProduct.isInterestRecalculationEnabled()) { + throw new EqualAmortizationUnsupportedFeatureException("interest.recalculation", "interest recalculation"); + } + } + + BigDecimal fixedPrincipalPercentagePerInstallment = this.fromApiJsonHelper + .extractBigDecimalWithLocaleNamed(LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName, element); + baseDataValidator.reset().parameter(LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName) + .value(fixedPrincipalPercentagePerInstallment).notLessThanMin(BigDecimal.ONE) + .notGreaterThanMax(BigDecimal.valueOf(100)); + + baseDataValidator.reset().parameter(LoanApiConstants.productIdParameterName).value(productId).notNull() + .integerGreaterThanZero(); + + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.accountNoParameterName, element)) { + final String accountNo = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.accountNoParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.accountNoParameterName).value(accountNo).ignoreIfNull() + .notExceedingLengthOf(20); + } + + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.externalIdParameterName, element)) { + final String externalId = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.externalIdParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.externalIdParameterName).value(externalId).ignoreIfNull() + .notExceedingLengthOf(100); + } + + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.fundIdParameterName, element)) { + final Long fundId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.fundIdParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.fundIdParameterName).value(fundId).ignoreIfNull() + .integerGreaterThanZero(); + } + + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.loanOfficerIdParameterName, element)) { + final Long loanOfficerId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.loanOfficerIdParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.loanOfficerIdParameterName).value(loanOfficerId).ignoreIfNull() + .integerGreaterThanZero(); + } + + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.loanPurposeIdParameterName, element)) { + final Long loanPurposeId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.loanPurposeIdParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.loanPurposeIdParameterName).value(loanPurposeId).ignoreIfNull() + .integerGreaterThanZero(); + } + + final BigDecimal principal = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.principalParamName, + element); + baseDataValidator.reset().parameter(LoanApiConstants.principalParamName).value(principal).notNull().positiveAmount(); + + final Integer loanTermFrequency = this.fromApiJsonHelper + .extractIntegerWithLocaleNamed(LoanApiConstants.loanTermFrequencyParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.loanTermFrequencyParameterName).value(loanTermFrequency).notNull() + .integerGreaterThanZero(); + + final Integer loanTermFrequencyType = this.fromApiJsonHelper + .extractIntegerSansLocaleNamed(LoanApiConstants.loanTermFrequencyTypeParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.loanTermFrequencyTypeParameterName).value(loanTermFrequencyType).notNull() + .inMinMaxRange(0, 3); + + final Integer numberOfRepayments = this.fromApiJsonHelper + .extractIntegerWithLocaleNamed(LoanApiConstants.numberOfRepaymentsParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.numberOfRepaymentsParameterName).value(numberOfRepayments).notNull() + .integerGreaterThanZero(); + + final Integer repaymentEvery = this.fromApiJsonHelper + .extractIntegerWithLocaleNamed(LoanApiConstants.repaymentEveryParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.repaymentEveryParameterName).value(repaymentEvery).notNull() + .integerGreaterThanZero(); + + final Integer repaymentEveryType = this.fromApiJsonHelper + .extractIntegerSansLocaleNamed(LoanApiConstants.repaymentFrequencyTypeParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.repaymentFrequencyTypeParameterName).value(repaymentEveryType).notNull() + .inMinMaxRange(0, 3); + + CalendarUtils.validateNthDayOfMonthFrequency(baseDataValidator, LoanApiConstants.repaymentFrequencyNthDayTypeParameterName, + LoanApiConstants.repaymentFrequencyDayOfWeekTypeParameterName, element, this.fromApiJsonHelper); + + final Integer interestType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(LoanApiConstants.interestTypeParameterName, + element); + baseDataValidator.reset().parameter(LoanApiConstants.interestTypeParameterName).value(interestType).notNull().inMinMaxRange(0, + 1); + + final Integer interestCalculationPeriodType = this.fromApiJsonHelper + .extractIntegerSansLocaleNamed(LoanApiConstants.interestCalculationPeriodTypeParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.interestCalculationPeriodTypeParameterName) + .value(interestCalculationPeriodType).notNull().inMinMaxRange(0, 1); + + boolean isInterestBearing = false; + if (loanProduct.isLinkedToFloatingInterestRate()) { + if (isEqualAmortization) { + throw new EqualAmortizationUnsupportedFeatureException("floating.interest.rate", "floating interest rate"); + } + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.interestRatePerPeriodParameterName, element)) { + baseDataValidator.reset().parameter(LoanApiConstants.interestRatePerPeriodParameterName).failWithCode( + "not.supported.loanproduct.linked.to.floating.rate", + "interestRatePerPeriod param is not supported, selected Loan Product is linked with floating interest rate."); + } + + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.isFloatingInterestRate, element)) { + final Boolean isFloatingInterestRate = this.fromApiJsonHelper + .extractBooleanNamed(LoanApiConstants.isFloatingInterestRate, element); + if (isFloatingInterestRate != null && isFloatingInterestRate + && !loanProduct.getFloatingRates().isFloatingInterestRateCalculationAllowed()) { + baseDataValidator.reset().parameter(LoanApiConstants.isFloatingInterestRate).failWithCode( + "true.not.supported.for.selected.loanproduct", + "isFloatingInterestRate value of true not supported for selected Loan Product."); } } else { - baseDataValidator.reset().parameter(LoanApiConstants.collateralParameterName).expectedArrayButIsNot(); + baseDataValidator.reset().parameter(LoanApiConstants.isFloatingInterestRate).trueOrFalseRequired(false); } - } - } - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.fixedEmiAmountParameterName, element)) { - if (!(loanProduct.isCanDefineInstallmentAmount() || loanProduct.isMultiDisburseLoan())) { - List unsupportedParameterList = new ArrayList<>(); - unsupportedParameterList.add(LoanApiConstants.fixedEmiAmountParameterName); - throw new UnsupportedParameterException(unsupportedParameterList); - } - if (isEqualAmortization) { - throw new EqualAmortizationUnsupportedFeatureException("fixed.emi", "fixed emi"); - } - final BigDecimal emiAmount = this.fromApiJsonHelper - .extractBigDecimalWithLocaleNamed(LoanApiConstants.fixedEmiAmountParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.fixedEmiAmountParameterName).value(emiAmount).ignoreIfNull() - .positiveAmount(); - } - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.maxOutstandingBalanceParameterName, element)) { - final BigDecimal maxOutstandingBalance = this.fromApiJsonHelper - .extractBigDecimalWithLocaleNamed(LoanApiConstants.maxOutstandingBalanceParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.maxOutstandingBalanceParameterName).value(maxOutstandingBalance) - .ignoreIfNull().positiveAmount(); - } - - if (loanProduct.isCanUseForTopup() && this.fromApiJsonHelper.parameterExists(LoanApiConstants.isTopup, element)) { - final Boolean isTopup = this.fromApiJsonHelper.extractBooleanNamed(LoanApiConstants.isTopup, element); - baseDataValidator.reset().parameter(LoanApiConstants.isTopup).value(isTopup).validateForBooleanValue(); - - if (isTopup != null && isTopup) { - final Long loanId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.loanIdToClose, element); - baseDataValidator.reset().parameter(LoanApiConstants.loanIdToClose).value(loanId).notNull().longGreaterThanZero(); - - if (clientId != null) { - final Long loanIdToClose = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.loanIdToClose, element); - final Loan loanToClose = this.loanRepositoryWrapper.findNonClosedLoanThatBelongsToClient(loanIdToClose, clientId); - if (loanToClose == null) { - throw new GeneralPlatformDomainRuleException( - "error.msg.loan.loanIdToClose.no.active.loan.associated.to.client.found", - "loanIdToClose is invalid, No Active Loan associated with the given Client ID found."); - } - if (loanToClose.isMultiDisburmentLoan() && !loanToClose.isInterestRecalculationEnabledForProduct()) { - throw new GeneralPlatformDomainRuleException( - "error.msg.loan.topup.on.multi.tranche.loan.without.interest.recalculation.not.supported", - "Topup on loan with multi-tranche disbursal and without interest recalculation is not supported."); - } - final LocalDate disbursalDateOfLoanToClose = loanToClose.getDisbursementDate(); - if (!DateUtils.isAfter(submittedOnDate, disbursalDateOfLoanToClose)) { - throw new GeneralPlatformDomainRuleException( - "error.msg.loan.submitted.date.should.be.after.topup.loan.disbursal.date", - "Submitted date of this loan application " + submittedOnDate - + " should be after the disbursed date of loan to be closed " + disbursalDateOfLoanToClose); - } - if (!loanToClose.getCurrencyCode().equals(loanProduct.getCurrency().getCode())) { - throw new GeneralPlatformDomainRuleException("error.msg.loan.to.be.closed.has.different.currency", - "loanIdToClose is invalid, Currency code is different."); - } - final LocalDate lastUserTransactionOnLoanToClose = loanToClose.getLastUserTransactionDate(); - if (DateUtils.isBefore(expectedDisbursementDate, lastUserTransactionOnLoanToClose)) { - throw new GeneralPlatformDomainRuleException( - "error.msg.loan.disbursal.date.should.be.after.last.transaction.date.of.loan.to.be.closed", - "Disbursal date of this loan application " + expectedDisbursementDate - + " should be after last transaction date of loan to be closed " - + lastUserTransactionOnLoanToClose); - } - BigDecimal loanOutstanding = this.loanReadPlatformService - .retrieveLoanPrePaymentTemplate(LoanTransactionType.REPAYMENT, loanIdToClose, expectedDisbursementDate) - .getAmount(); - if (loanOutstanding.compareTo(principal) > 0) { - throw new GeneralPlatformDomainRuleException("error.msg.loan.amount.less.than.outstanding.of.loan.to.be.closed", - "Topup loan amount should be greater than outstanding amount of loan to be closed."); - } + if (interestType != null && interestType.equals(InterestMethod.FLAT.getValue())) { + baseDataValidator.reset().parameter(LoanApiConstants.interestTypeParameterName).failWithCode( + "should.be.0.for.selected.loan.product", + "interestType should be DECLINING_BALANCE for selected Loan Product as it is linked to floating rates."); } + + final String interestRateDifferentialParameterName = LoanApiConstants.interestRateDifferential; + final BigDecimal interestRateDifferential = this.fromApiJsonHelper + .extractBigDecimalWithLocaleNamed(interestRateDifferentialParameterName, element); + baseDataValidator.reset().parameter(interestRateDifferentialParameterName).value(interestRateDifferential).notNull() + .zeroOrPositiveAmount().inMinAndMaxAmountRange(loanProduct.getFloatingRates().getMinDifferentialLendingRate(), + loanProduct.getFloatingRates().getMaxDifferentialLendingRate()); + isInterestBearing = true; + } else { + + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.isFloatingInterestRate, element)) { + baseDataValidator.reset().parameter(LoanApiConstants.isFloatingInterestRate).failWithCode( + "not.supported.loanproduct.not.linked.to.floating.rate", + "isFloatingInterestRate param is not supported, selected Loan Product is not linked with floating interest rate."); + } + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.interestRateDifferential, element)) { + baseDataValidator.reset().parameter(LoanApiConstants.interestRateDifferential).failWithCode( + "not.supported.loanproduct.not.linked.to.floating.rate", + "interestRateDifferential param is not supported, selected Loan Product is not linked with floating interest rate."); + } + + final BigDecimal interestRatePerPeriod = this.fromApiJsonHelper + .extractBigDecimalWithLocaleNamed(LoanApiConstants.interestRatePerPeriodParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.interestRatePerPeriodParameterName).value(interestRatePerPeriod) + .notNull().zeroOrPositiveAmount(); + isInterestBearing = interestRatePerPeriod.compareTo(BigDecimal.ZERO) > 0; } - } - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.datatables, element)) { - final JsonArray datatables = this.fromApiJsonHelper.extractJsonArrayNamed(LoanApiConstants.datatables, element); - baseDataValidator.reset().parameter(LoanApiConstants.datatables).value(datatables).notNull().jsonArrayNotEmpty(); - } - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.daysInYearTypeParameterName, element)) { - final Integer daysInYearType = this.fromApiJsonHelper.extractIntegerNamed(LoanApiConstants.daysInYearTypeParameterName, element, - Locale.getDefault()); - baseDataValidator.reset().parameter(LoanApiConstants.daysInYearTypeParameterName).value(daysInYearType).notNull() - .isOneOfTheseValues(1, 360, 364, 365); - } + final Integer amortizationType = this.fromApiJsonHelper + .extractIntegerSansLocaleNamed(LoanApiConstants.amortizationTypeParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.amortizationTypeParameterName).value(amortizationType).notNull() + .inMinMaxRange(0, 1); - validateLoanMultiDisbursementDate(element, baseDataValidator, expectedDisbursementDate, principal); + if (!AmortizationMethod.EQUAL_PRINCIPAL.getValue().equals(amortizationType) && fixedPrincipalPercentagePerInstallment != null) { + baseDataValidator.reset().parameter(LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName).failWithCode( + "not.supported.principal.fixing.not.allowed.with.equal.installments", + "Principal fixing cannot be done with equal installment amortization"); + } - String loanScheduleProcessingType = loanProduct.getLoanProductRelatedDetail().getLoanScheduleProcessingType().name(); - if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE, element)) { - loanScheduleProcessingType = this.fromApiJsonHelper.extractStringNamed(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE, + final LocalDate expectedDisbursementDate = this.fromApiJsonHelper + .extractLocalDateNamed(LoanApiConstants.expectedDisbursementDateParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.expectedDisbursementDateParameterName).value(expectedDisbursementDate) + .notNull(); + + // grace validation + final Integer graceOnPrincipalPayment = this.fromApiJsonHelper + .extractIntegerWithLocaleNamed(LoanApiConstants.graceOnPrincipalPaymentParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.graceOnPrincipalPaymentParameterName).value(graceOnPrincipalPayment) + .zeroOrPositiveAmount(); + + final Integer graceOnInterestPayment = this.fromApiJsonHelper + .extractIntegerWithLocaleNamed(LoanApiConstants.graceOnInterestPaymentParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.graceOnInterestPaymentParameterName).value(graceOnInterestPayment) + .zeroOrPositiveAmount(); + + final Integer graceOnInterestCharged = this.fromApiJsonHelper + .extractIntegerWithLocaleNamed(LoanApiConstants.graceOnInterestChargedParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.graceOnInterestChargedParameterName).value(graceOnInterestCharged) + .zeroOrPositiveAmount(); + + final Integer graceOnArrearsAgeing = this.fromApiJsonHelper + .extractIntegerWithLocaleNamed(LoanProductConstants.GRACE_ON_ARREARS_AGEING_PARAMETER_NAME, element); + baseDataValidator.reset().parameter(LoanProductConstants.GRACE_ON_ARREARS_AGEING_PARAMETER_NAME).value(graceOnArrearsAgeing) + .zeroOrPositiveAmount(); + + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.interestChargedFromDateParameterName, element)) { + final LocalDate interestChargedFromDate = this.fromApiJsonHelper + .extractLocalDateNamed(LoanApiConstants.interestChargedFromDateParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.interestChargedFromDateParameterName).value(interestChargedFromDate) + .ignoreIfNull().notNull(); + } + + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.repaymentsStartingFromDateParameterName, element)) { + final LocalDate repaymentsStartingFromDate = this.fromApiJsonHelper + .extractLocalDateNamed(LoanApiConstants.repaymentsStartingFromDateParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.repaymentsStartingFromDateParameterName) + .value(repaymentsStartingFromDate).ignoreIfNull().notNull(); + } + + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.inArrearsToleranceParameterName, element)) { + final BigDecimal inArrearsTolerance = this.fromApiJsonHelper + .extractBigDecimalWithLocaleNamed(LoanApiConstants.inArrearsToleranceParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.inArrearsToleranceParameterName).value(inArrearsTolerance) + .ignoreIfNull().zeroOrPositiveAmount(); + } + + final LocalDate submittedOnDate = this.fromApiJsonHelper.extractLocalDateNamed(LoanApiConstants.submittedOnDateParameterName, element); - baseDataValidator.reset().parameter(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE).value(loanScheduleProcessingType) - .isOneOfEnumValues(LoanScheduleProcessingType.class); - } - if (LoanScheduleProcessingType.VERTICAL.equals(LoanScheduleProcessingType.valueOf(loanScheduleProcessingType)) - && !AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY - .equals(transactionProcessingStrategy)) { - baseDataValidator.reset().parameter(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE).failWithCode( - "supported.only.with.advanced.payment.allocation.strategy", - "Vertical repayment schedule processing is only available with `Advanced payment allocation` strategy"); - } - List allocationRules = loanProduct.getPaymentAllocationRules(); + baseDataValidator.reset().parameter(LoanApiConstants.submittedOnDateParameterName).value(submittedOnDate).notNull(); - if (LoanScheduleProcessingType.HORIZONTAL.name().equals(loanScheduleProcessingType) - && AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY.equals(transactionProcessingStrategy)) { - advancedPaymentAllocationsValidator.checkGroupingOfAllocationRules(allocationRules); - } + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.submittedOnNoteParameterName, element)) { + final String submittedOnNote = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.submittedOnNoteParameterName, + element); + baseDataValidator.reset().parameter(LoanApiConstants.submittedOnNoteParameterName).value(submittedOnNote).ignoreIfNull() + .notExceedingLengthOf(500); + } - validatePartialPeriodSupport(interestCalculationPeriodType, baseDataValidator, element, loanProduct); + final String transactionProcessingStrategy = this.fromApiJsonHelper + .extractStringNamed(LoanApiConstants.transactionProcessingStrategyCodeParameterName, element); - // validate enable installment level delinquency - if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY, element)) { - final Boolean isEnableInstallmentLevelDelinquency = this.fromApiJsonHelper - .extractBooleanNamed(LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY, element); - baseDataValidator.reset().parameter(LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY) - .value(isEnableInstallmentLevelDelinquency).validateForBooleanValue(); - if (loanProduct.getDelinquencyBucket() == null) { - if (isEnableInstallmentLevelDelinquency) { - baseDataValidator.reset().parameter(LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY).failWithCode( - "can.be.enabled.for.loan.with.loan.product.having.valid.delinquency.bucket", - "Installment level delinquency cannot be enabled for a loan if Delinquency bucket is not configured for loan product"); + validateTransactionProcessingStrategy(transactionProcessingStrategy, loanProduct, baseDataValidator); + + validateLinkedSavingsAccount(element, baseDataValidator); + + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.createStandingInstructionAtDisbursementParameterName, element)) { + final Boolean createStandingInstructionAtDisbursement = this.fromApiJsonHelper + .extractBooleanNamed(LoanApiConstants.createStandingInstructionAtDisbursementParameterName, element); + final Long linkAccountId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.linkAccountIdParameterName, element); + + if (createStandingInstructionAtDisbursement) { + baseDataValidator.reset().parameter(LoanApiConstants.linkAccountIdParameterName).value(linkAccountId).notNull() + .longGreaterThanZero(); } } - } - validateBorrowerCycle(element, loanProduct, clientId, groupId, baseDataValidator); + // charges + loanChargeApiJsonValidator.validateLoanCharges(element, loanProduct, baseDataValidator); - loanProductDataValidator.fixedLengthValidations(transactionProcessingStrategy, isInterestBearing, numberOfRepayments, - repaymentEvery, element, baseDataValidator); + /** + * TODO: Add collaterals for other loan accounts if needed. For now it's only applicable for individual + * accounts. (loanType.isJLG() || loanType.isGLIM()) + */ - // Validate If the externalId is already registered - final String externalIdStr = this.fromApiJsonHelper.extractStringNamed("externalId", element); - ExternalId externalId = ExternalIdFactory.produce(externalIdStr); - if (!externalId.isEmpty()) { - final boolean existByExternalId = this.loanRepositoryWrapper.existLoanByExternalId(externalId); - if (existByExternalId) { - throw new GeneralPlatformDomainRuleException("error.msg.loan.with.externalId.already.used", - "Loan with externalId is already registered."); + if (!StringUtils.isBlank(loanTypeStr)) { + final AccountType loanType = AccountType.fromName(loanTypeStr); + + // collateral + if (loanType.isIndividualAccount() && element.isJsonObject() + && this.fromApiJsonHelper.parameterExists(LoanApiConstants.collateralParameterName, element)) { + final JsonObject topLevelJsonElement = element.getAsJsonObject(); + final Locale locale = this.fromApiJsonHelper.extractLocaleParameter(topLevelJsonElement); + if (topLevelJsonElement.get(LoanApiConstants.collateralParameterName).isJsonArray()) { + + final Type collateralParameterTypeOfMap = new TypeToken>() { + + }.getType(); + final Set supportedParameters = new HashSet<>( + Arrays.asList(LoanApiConstants.clientCollateralIdParameterName, LoanApiConstants.quantityParameterName)); + final JsonArray array = topLevelJsonElement.get(LoanApiConstants.collateralParameterName).getAsJsonArray(); + for (int i = 1; i <= array.size(); i++) { + final JsonObject collateralItemElement = array.get(i - 1).getAsJsonObject(); + + final String collateralJson = this.fromApiJsonHelper.toJson(collateralItemElement); + this.fromApiJsonHelper.checkForUnsupportedParameters(collateralParameterTypeOfMap, collateralJson, + supportedParameters); + + final Long clientCollateralId = this.fromApiJsonHelper + .extractLongNamed(LoanApiConstants.clientCollateralIdParameterName, collateralItemElement); + baseDataValidator.reset().parameter(LoanApiConstants.collateralParameterName) + .parameterAtIndexArray(LoanApiConstants.clientCollateralIdParameterName, i).value(clientCollateralId) + .notNull().integerGreaterThanZero(); + + final BigDecimal quantity = this.fromApiJsonHelper + .extractBigDecimalNamed(LoanApiConstants.quantityParameterName, collateralItemElement, locale); + baseDataValidator.reset().parameter(LoanApiConstants.collateralParameterName) + .parameterAtIndexArray(LoanApiConstants.quantityParameterName, i).value(quantity).notNull() + .positiveAmount(); + + final ClientCollateralManagement clientCollateralManagement = this.clientCollateralManagementRepositoryWrapper + .getCollateral(clientCollateralId); + + if (clientCollateralId != null + && BigDecimal.valueOf(0).compareTo(clientCollateralManagement.getQuantity()) >= 0) { + throw new InvalidAmountOfCollateralQuantity(clientCollateralManagement.getQuantity()); + } + + } + } else { + baseDataValidator.reset().parameter(LoanApiConstants.collateralParameterName).expectedArrayButIsNot(); + } + } } - } - checkForProductMixRestrictions(element); - validateSubmittedOnDate(element, null, loanProduct); - validateDisbursementDetails(loanProduct, element); - validateCollateral(element); - // validate if disbursement date is a holiday or a non-working day - validateDisbursementDateIsOnNonWorkingDay(expectedDisbursementDate); - Long officeId = client != null ? client.getOffice().getId() : group.getOffice().getId(); - validateDisbursementDateIsOnHoliday(expectedDisbursementDate, officeId); - final Integer recurringMoratoriumOnPrincipalPeriods = this.fromApiJsonHelper - .extractIntegerWithLocaleNamed("recurringMoratoriumOnPrincipalPeriods", element); - loanProductDataValidator.validateRepaymentPeriodWithGraceSettings(numberOfRepayments, graceOnPrincipalPayment, - graceOnInterestPayment, graceOnInterestCharged, recurringMoratoriumOnPrincipalPeriods, baseDataValidator); + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.fixedEmiAmountParameterName, element)) { + if (!(loanProduct.isCanDefineInstallmentAmount() || loanProduct.isMultiDisburseLoan())) { + List unsupportedParameterList = new ArrayList<>(); + unsupportedParameterList.add(LoanApiConstants.fixedEmiAmountParameterName); + throw new UnsupportedParameterException(unsupportedParameterList); + } + if (isEqualAmortization) { + throw new EqualAmortizationUnsupportedFeatureException("fixed.emi", "fixed emi"); + } + final BigDecimal emiAmount = this.fromApiJsonHelper + .extractBigDecimalWithLocaleNamed(LoanApiConstants.fixedEmiAmountParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.fixedEmiAmountParameterName).value(emiAmount).ignoreIfNull() + .positiveAmount(); + } + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.maxOutstandingBalanceParameterName, element)) { + final BigDecimal maxOutstandingBalance = this.fromApiJsonHelper + .extractBigDecimalWithLocaleNamed(LoanApiConstants.maxOutstandingBalanceParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.maxOutstandingBalanceParameterName).value(maxOutstandingBalance) + .ignoreIfNull().positiveAmount(); + } - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } + if (loanProduct.isCanUseForTopup() && this.fromApiJsonHelper.parameterExists(LoanApiConstants.isTopup, element)) { + final Boolean isTopup = this.fromApiJsonHelper.extractBooleanNamed(LoanApiConstants.isTopup, element); + baseDataValidator.reset().parameter(LoanApiConstants.isTopup).value(isTopup).validateForBooleanValue(); + + if (isTopup != null && isTopup) { + final Long loanId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.loanIdToClose, element); + baseDataValidator.reset().parameter(LoanApiConstants.loanIdToClose).value(loanId).notNull().longGreaterThanZero(); + + if (clientId != null) { + final Long loanIdToClose = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.loanIdToClose, element); + final Loan loanToClose = this.loanRepositoryWrapper.findNonClosedLoanThatBelongsToClient(loanIdToClose, clientId); + if (loanToClose == null) { + throw new GeneralPlatformDomainRuleException( + "error.msg.loan.loanIdToClose.no.active.loan.associated.to.client.found", + "loanIdToClose is invalid, No Active Loan associated with the given Client ID found."); + } + if (loanToClose.isMultiDisburmentLoan() && !loanToClose.isInterestRecalculationEnabledForProduct()) { + throw new GeneralPlatformDomainRuleException( + "error.msg.loan.topup.on.multi.tranche.loan.without.interest.recalculation.not.supported", + "Topup on loan with multi-tranche disbursal and without interest recalculation is not supported."); + } + final LocalDate disbursalDateOfLoanToClose = loanToClose.getDisbursementDate(); + if (!DateUtils.isAfter(submittedOnDate, disbursalDateOfLoanToClose)) { + throw new GeneralPlatformDomainRuleException( + "error.msg.loan.submitted.date.should.be.after.topup.loan.disbursal.date", + "Submitted date of this loan application " + submittedOnDate + + " should be after the disbursed date of loan to be closed " + disbursalDateOfLoanToClose); + } + if (!loanToClose.getCurrencyCode().equals(loanProduct.getCurrency().getCode())) { + throw new GeneralPlatformDomainRuleException("error.msg.loan.to.be.closed.has.different.currency", + "loanIdToClose is invalid, Currency code is different."); + } + final LocalDate lastUserTransactionOnLoanToClose = loanToClose.getLastUserTransactionDate(); + if (DateUtils.isBefore(expectedDisbursementDate, lastUserTransactionOnLoanToClose)) { + throw new GeneralPlatformDomainRuleException( + "error.msg.loan.disbursal.date.should.be.after.last.transaction.date.of.loan.to.be.closed", + "Disbursal date of this loan application " + expectedDisbursementDate + + " should be after last transaction date of loan to be closed " + + lastUserTransactionOnLoanToClose); + } + BigDecimal loanOutstanding = this.loanReadPlatformService + .retrieveLoanPrePaymentTemplate(LoanTransactionType.REPAYMENT, loanIdToClose, expectedDisbursementDate) + .getAmount(); + if (loanOutstanding.compareTo(principal) > 0) { + throw new GeneralPlatformDomainRuleException("error.msg.loan.amount.less.than.outstanding.of.loan.to.be.closed", + "Topup loan amount should be greater than outstanding amount of loan to be closed."); + } + } + } + } + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.datatables, element)) { + final JsonArray datatables = this.fromApiJsonHelper.extractJsonArrayNamed(LoanApiConstants.datatables, element); + baseDataValidator.reset().parameter(LoanApiConstants.datatables).value(datatables).notNull().jsonArrayNotEmpty(); + } + + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.daysInYearTypeParameterName, element)) { + final Integer daysInYearType = this.fromApiJsonHelper.extractIntegerNamed(LoanApiConstants.daysInYearTypeParameterName, + element, Locale.getDefault()); + baseDataValidator.reset().parameter(LoanApiConstants.daysInYearTypeParameterName).value(daysInYearType).notNull() + .isOneOfTheseValues(1, 360, 364, 365); + } + + validateLoanMultiDisbursementDate(element, baseDataValidator, expectedDisbursementDate, principal); + + String loanScheduleProcessingType = loanProduct.getLoanProductRelatedDetail().getLoanScheduleProcessingType().name(); + if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE, element)) { + loanScheduleProcessingType = this.fromApiJsonHelper.extractStringNamed(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE, + element); + baseDataValidator.reset().parameter(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE).value(loanScheduleProcessingType) + .isOneOfEnumValues(LoanScheduleProcessingType.class); + } + if (LoanScheduleProcessingType.VERTICAL.equals(LoanScheduleProcessingType.valueOf(loanScheduleProcessingType)) + && !AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY + .equals(transactionProcessingStrategy)) { + baseDataValidator.reset().parameter(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE).failWithCode( + "supported.only.with.advanced.payment.allocation.strategy", + "Vertical repayment schedule processing is only available with `Advanced payment allocation` strategy"); + } + + List allocationRules = loanProduct.getPaymentAllocationRules(); + + if (LoanScheduleProcessingType.HORIZONTAL.name().equals(loanScheduleProcessingType) + && AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY + .equals(transactionProcessingStrategy)) { + advancedPaymentAllocationsValidator.checkGroupingOfAllocationRules(allocationRules); + } + + validatePartialPeriodSupport(interestCalculationPeriodType, baseDataValidator, element, loanProduct); + + // validate enable installment level delinquency + if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY, element)) { + final Boolean isEnableInstallmentLevelDelinquency = this.fromApiJsonHelper + .extractBooleanNamed(LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY, element); + baseDataValidator.reset().parameter(LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY) + .value(isEnableInstallmentLevelDelinquency).validateForBooleanValue(); + if (loanProduct.getDelinquencyBucket() == null) { + if (isEnableInstallmentLevelDelinquency) { + baseDataValidator.reset().parameter(LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY).failWithCode( + "can.be.enabled.for.loan.with.loan.product.having.valid.delinquency.bucket", + "Installment level delinquency cannot be enabled for a loan if Delinquency bucket is not configured for loan product"); + } + } + } + + validateBorrowerCycle(element, loanProduct, clientId, groupId, baseDataValidator); + + loanProductDataValidator.fixedLengthValidations(transactionProcessingStrategy, isInterestBearing, numberOfRepayments, + repaymentEvery, element, baseDataValidator); + + // Validate If the externalId is already registered + final String externalIdStr = this.fromApiJsonHelper.extractStringNamed("externalId", element); + ExternalId externalId = ExternalIdFactory.produce(externalIdStr); + if (!externalId.isEmpty()) { + final boolean existByExternalId = this.loanRepositoryWrapper.existLoanByExternalId(externalId); + if (existByExternalId) { + throw new GeneralPlatformDomainRuleException("error.msg.loan.with.externalId.already.used", + "Loan with externalId is already registered."); + } + } + + checkForProductMixRestrictions(element); + validateSubmittedOnDate(element, null, loanProduct); + validateDisbursementDetails(loanProduct, element); + validateCollateral(element); + // validate if disbursement date is a holiday or a non-working day + validateDisbursementDateIsOnNonWorkingDay(expectedDisbursementDate); + Long officeId = client != null ? client.getOffice().getId() : group.getOffice().getId(); + validateDisbursementDateIsOnHoliday(expectedDisbursementDate, officeId); + final Integer recurringMoratoriumOnPrincipalPeriods = this.fromApiJsonHelper + .extractIntegerWithLocaleNamed("recurringMoratoriumOnPrincipalPeriods", element); + loanProductDataValidator.validateRepaymentPeriodWithGraceSettings(numberOfRepayments, graceOnPrincipalPayment, + graceOnInterestPayment, graceOnInterestCharged, recurringMoratoriumOnPrincipalPeriods, baseDataValidator); + }); } private void validateBorrowerCycle(JsonElement element, LoanProduct loanProduct, Long clientId, Long groupId, @@ -821,577 +820,585 @@ public final class LoanApplicationValidator { loanProduct = this.loanProductRepository.findById(productId).orElseThrow(() -> new LoanProductNotFoundException(productId)); } - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan"); - final JsonElement element = this.fromApiJsonHelper.parse(json); - boolean atLeastOneParameterPassedForUpdate = false; + validateOrThrow("loan", baseDataValidator -> { + final JsonElement element = this.fromApiJsonHelper.parse(json); + boolean atLeastOneParameterPassedForUpdate = false; - Long clientId = loan.getClient() != null ? loan.getClient().getId() : null; - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.clientIdParameterName, element)) { - atLeastOneParameterPassedForUpdate = true; - clientId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.clientIdParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.clientIdParameterName).value(clientId).notNull().integerGreaterThanZero(); - } - Client client = null; - if (clientId != null) { - client = this.clientRepository.findOneWithNotFoundDetection(clientId); - } - Long groupId = loan.getGroup() != null ? loan.getGroup().getId() : null; - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.groupIdParameterName, element)) { - atLeastOneParameterPassedForUpdate = true; - groupId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.groupIdParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.groupIdParameterName).value(groupId).notNull().integerGreaterThanZero(); - } - Group group = null; - if (groupId != null) { - group = this.groupRepository.findOneWithNotFoundDetection(groupId); - } - - if (productId != null) { - atLeastOneParameterPassedForUpdate = true; - baseDataValidator.reset().parameter(LoanApiConstants.productIdParameterName).value(productId).notNull() - .integerGreaterThanZero(); - } - - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.accountNoParameterName, element)) { - atLeastOneParameterPassedForUpdate = true; - final String accountNo = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.accountNoParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.accountNoParameterName).value(accountNo).notBlank() - .notExceedingLengthOf(20); - } - - boolean isEqualAmortization = loan.getLoanProductRelatedDetail().isEqualAmortization(); - if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.IS_EQUAL_AMORTIZATION_PARAM, element)) { - isEqualAmortization = this.fromApiJsonHelper.extractBooleanNamed(LoanProductConstants.IS_EQUAL_AMORTIZATION_PARAM, element); - baseDataValidator.reset().parameter(LoanProductConstants.IS_EQUAL_AMORTIZATION_PARAM).value(isEqualAmortization).ignoreIfNull() - .validateForBooleanValue(); - if (isEqualAmortization && loanProduct.isInterestRecalculationEnabled()) { - throw new EqualAmortizationUnsupportedFeatureException("interest.recalculation", "interest recalculation"); - } - } - - BigDecimal fixedPrincipalPercentagePerInstallment = this.fromApiJsonHelper - .extractBigDecimalWithLocaleNamed(LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName, element); - baseDataValidator.reset().parameter(LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName) - .value(fixedPrincipalPercentagePerInstallment).notLessThanMin(BigDecimal.ONE).notGreaterThanMax(BigDecimal.valueOf(100)); - - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.externalIdParameterName, element)) { - atLeastOneParameterPassedForUpdate = true; - final String externalId = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.externalIdParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.externalIdParameterName).value(externalId).ignoreIfNull() - .notExceedingLengthOf(100); - } - - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.fundIdParameterName, element)) { - atLeastOneParameterPassedForUpdate = true; - final Long fundId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.fundIdParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.fundIdParameterName).value(fundId).ignoreIfNull().integerGreaterThanZero(); - } - - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.loanOfficerIdParameterName, element)) { - atLeastOneParameterPassedForUpdate = true; - final Long loanOfficerId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.loanOfficerIdParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.loanOfficerIdParameterName).value(loanOfficerId).ignoreIfNull() - .integerGreaterThanZero(); - } - - String transactionProcessingStrategy = loan.getTransactionProcessingStrategyCode(); - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.transactionProcessingStrategyCodeParameterName, element)) { - atLeastOneParameterPassedForUpdate = true; - transactionProcessingStrategy = this.fromApiJsonHelper - .extractStringNamed(LoanApiConstants.transactionProcessingStrategyCodeParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.transactionProcessingStrategyCodeParameterName) - .value(transactionProcessingStrategy).notNull(); - // Validating whether the processor is existing - validateTransactionProcessingStrategy(transactionProcessingStrategy, loanProduct, baseDataValidator); - } - - if (!AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY - .equals(loanProduct.getTransactionProcessingStrategyCode()) - && AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY.equals(transactionProcessingStrategy)) { - baseDataValidator.reset().parameter(LoanApiConstants.transactionProcessingStrategyCodeParameterName).failWithCode( - "strategy.cannot.be.advanced.payment.allocation.if.not.configured", - "Loan transaction processing strategy cannot be Advanced Payment Allocation Strategy if it's not configured on loan product"); - } - - BigDecimal principal = null; - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.principalParameterName, element)) { - atLeastOneParameterPassedForUpdate = true; - principal = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.principalParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.principalParameterName).value(principal).notNull().positiveAmount(); - } - - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.inArrearsToleranceParameterName, element)) { - atLeastOneParameterPassedForUpdate = true; - final BigDecimal inArrearsTolerance = this.fromApiJsonHelper - .extractBigDecimalWithLocaleNamed(LoanApiConstants.inArrearsToleranceParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.inArrearsToleranceParameterName).value(inArrearsTolerance).ignoreIfNull() - .zeroOrPositiveAmount(); - } - - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.loanTermFrequencyParameterName, element)) { - atLeastOneParameterPassedForUpdate = true; - final Integer loanTermFrequency = this.fromApiJsonHelper - .extractIntegerWithLocaleNamed(LoanApiConstants.loanTermFrequencyParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.loanTermFrequencyParameterName).value(loanTermFrequency).notNull() - .integerGreaterThanZero(); - } - - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.loanTermFrequencyTypeParameterName, element)) { - atLeastOneParameterPassedForUpdate = true; - final Integer loanTermFrequencyType = this.fromApiJsonHelper - .extractIntegerWithLocaleNamed(LoanApiConstants.loanTermFrequencyTypeParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.loanTermFrequencyTypeParameterName).value(loanTermFrequencyType).notNull() - .inMinMaxRange(0, 3); - } - - Integer numberOfRepayments = loan.getNumberOfRepayments(); - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.numberOfRepaymentsParameterName, element)) { - atLeastOneParameterPassedForUpdate = true; - numberOfRepayments = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(LoanApiConstants.numberOfRepaymentsParameterName, - element); - baseDataValidator.reset().parameter(LoanApiConstants.numberOfRepaymentsParameterName).value(numberOfRepayments).notNull() - .integerGreaterThanZero(); - } - - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.repaymentEveryParameterName, element)) { - atLeastOneParameterPassedForUpdate = true; - final Integer repaymentEvery = this.fromApiJsonHelper - .extractIntegerWithLocaleNamed(LoanApiConstants.repaymentEveryParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.repaymentEveryParameterName).value(repaymentEvery).notNull() - .integerGreaterThanZero(); - } - - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.repaymentFrequencyTypeParameterName, element)) { - atLeastOneParameterPassedForUpdate = true; - final Integer repaymentEveryType = this.fromApiJsonHelper - .extractIntegerWithLocaleNamed(LoanApiConstants.repaymentFrequencyTypeParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.repaymentFrequencyTypeParameterName).value(repaymentEveryType).notNull() - .inMinMaxRange(0, 3); - } - - CalendarUtils.validateNthDayOfMonthFrequency(baseDataValidator, LoanApiConstants.repaymentFrequencyNthDayTypeParameterName, - LoanApiConstants.repaymentFrequencyDayOfWeekTypeParameterName, element, this.fromApiJsonHelper); - - Integer interestType = null; - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.interestTypeParameterName, element)) { - atLeastOneParameterPassedForUpdate = true; - interestType = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(LoanApiConstants.interestTypeParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.interestTypeParameterName).value(interestType).notNull().inMinMaxRange(0, - 1); - } - - if (loanProduct.isLinkedToFloatingInterestRate()) { - if (isEqualAmortization) { - throw new EqualAmortizationUnsupportedFeatureException("floating.interest.rate", "floating interest rate"); - } - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.interestRatePerPeriodParameterName, element)) { - baseDataValidator.reset().parameter(LoanApiConstants.interestRatePerPeriodParameterName).failWithCode( - "not.supported.loanproduct.linked.to.floating.rate", - "interestRatePerPeriod param is not supported, selected Loan Product is linked with floating interest rate."); - } - - Boolean isFloatingInterestRate = loan.getIsFloatingInterestRate(); - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.isFloatingInterestRate, element)) { - isFloatingInterestRate = this.fromApiJsonHelper.extractBooleanNamed(LoanApiConstants.isFloatingInterestRate, element); + Long clientId = loan.getClient() != null ? loan.getClient().getId() : null; + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.clientIdParameterName, element)) { atLeastOneParameterPassedForUpdate = true; + clientId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.clientIdParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.clientIdParameterName).value(clientId).notNull() + .integerGreaterThanZero(); } - if (isFloatingInterestRate != null) { - if (isFloatingInterestRate && !loanProduct.getFloatingRates().isFloatingInterestRateCalculationAllowed()) { - baseDataValidator.reset().parameter(LoanApiConstants.isFloatingInterestRate).failWithCode( - "true.not.supported.for.selected.loanproduct", - "isFloatingInterestRate value of true not supported for selected Loan Product."); + Client client = null; + if (clientId != null) { + client = this.clientRepository.findOneWithNotFoundDetection(clientId); + } + Long groupId = loan.getGroup() != null ? loan.getGroup().getId() : null; + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.groupIdParameterName, element)) { + atLeastOneParameterPassedForUpdate = true; + groupId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.groupIdParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.groupIdParameterName).value(groupId).notNull() + .integerGreaterThanZero(); + } + Group group = null; + if (groupId != null) { + group = this.groupRepository.findOneWithNotFoundDetection(groupId); + } + + if (productId != null) { + atLeastOneParameterPassedForUpdate = true; + baseDataValidator.reset().parameter(LoanApiConstants.productIdParameterName).value(productId).notNull() + .integerGreaterThanZero(); + } + + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.accountNoParameterName, element)) { + atLeastOneParameterPassedForUpdate = true; + final String accountNo = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.accountNoParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.accountNoParameterName).value(accountNo).notBlank() + .notExceedingLengthOf(20); + } + + boolean isEqualAmortization = loan.getLoanProductRelatedDetail().isEqualAmortization(); + if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.IS_EQUAL_AMORTIZATION_PARAM, element)) { + isEqualAmortization = this.fromApiJsonHelper.extractBooleanNamed(LoanProductConstants.IS_EQUAL_AMORTIZATION_PARAM, element); + baseDataValidator.reset().parameter(LoanProductConstants.IS_EQUAL_AMORTIZATION_PARAM).value(isEqualAmortization) + .ignoreIfNull().validateForBooleanValue(); + if (isEqualAmortization && loanProduct.isInterestRecalculationEnabled()) { + throw new EqualAmortizationUnsupportedFeatureException("interest.recalculation", "interest recalculation"); } + } + + BigDecimal fixedPrincipalPercentagePerInstallment = this.fromApiJsonHelper + .extractBigDecimalWithLocaleNamed(LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName, element); + baseDataValidator.reset().parameter(LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName) + .value(fixedPrincipalPercentagePerInstallment).notLessThanMin(BigDecimal.ONE) + .notGreaterThanMax(BigDecimal.valueOf(100)); + + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.externalIdParameterName, element)) { + atLeastOneParameterPassedForUpdate = true; + final String externalId = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.externalIdParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.externalIdParameterName).value(externalId).ignoreIfNull() + .notExceedingLengthOf(100); + } + + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.fundIdParameterName, element)) { + atLeastOneParameterPassedForUpdate = true; + final Long fundId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.fundIdParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.fundIdParameterName).value(fundId).ignoreIfNull() + .integerGreaterThanZero(); + } + + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.loanOfficerIdParameterName, element)) { + atLeastOneParameterPassedForUpdate = true; + final Long loanOfficerId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.loanOfficerIdParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.loanOfficerIdParameterName).value(loanOfficerId).ignoreIfNull() + .integerGreaterThanZero(); + } + + String transactionProcessingStrategy = loan.getTransactionProcessingStrategyCode(); + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.transactionProcessingStrategyCodeParameterName, element)) { + atLeastOneParameterPassedForUpdate = true; + transactionProcessingStrategy = this.fromApiJsonHelper + .extractStringNamed(LoanApiConstants.transactionProcessingStrategyCodeParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.transactionProcessingStrategyCodeParameterName) + .value(transactionProcessingStrategy).notNull(); + // Validating whether the processor is existing + validateTransactionProcessingStrategy(transactionProcessingStrategy, loanProduct, baseDataValidator); + } + + if (!AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY + .equals(loanProduct.getTransactionProcessingStrategyCode()) + && AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY + .equals(transactionProcessingStrategy)) { + baseDataValidator.reset().parameter(LoanApiConstants.transactionProcessingStrategyCodeParameterName).failWithCode( + "strategy.cannot.be.advanced.payment.allocation.if.not.configured", + "Loan transaction processing strategy cannot be Advanced Payment Allocation Strategy if it's not configured on loan product"); + } + + BigDecimal principal = null; + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.principalParameterName, element)) { + atLeastOneParameterPassedForUpdate = true; + principal = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.principalParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.principalParameterName).value(principal).notNull().positiveAmount(); + } + + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.inArrearsToleranceParameterName, element)) { + atLeastOneParameterPassedForUpdate = true; + final BigDecimal inArrearsTolerance = this.fromApiJsonHelper + .extractBigDecimalWithLocaleNamed(LoanApiConstants.inArrearsToleranceParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.inArrearsToleranceParameterName).value(inArrearsTolerance) + .ignoreIfNull().zeroOrPositiveAmount(); + } + + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.loanTermFrequencyParameterName, element)) { + atLeastOneParameterPassedForUpdate = true; + final Integer loanTermFrequency = this.fromApiJsonHelper + .extractIntegerWithLocaleNamed(LoanApiConstants.loanTermFrequencyParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.loanTermFrequencyParameterName).value(loanTermFrequency).notNull() + .integerGreaterThanZero(); + } + + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.loanTermFrequencyTypeParameterName, element)) { + atLeastOneParameterPassedForUpdate = true; + final Integer loanTermFrequencyType = this.fromApiJsonHelper + .extractIntegerWithLocaleNamed(LoanApiConstants.loanTermFrequencyTypeParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.loanTermFrequencyTypeParameterName).value(loanTermFrequencyType) + .notNull().inMinMaxRange(0, 3); + } + + Integer numberOfRepayments = loan.getNumberOfRepayments(); + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.numberOfRepaymentsParameterName, element)) { + atLeastOneParameterPassedForUpdate = true; + numberOfRepayments = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(LoanApiConstants.numberOfRepaymentsParameterName, + element); + baseDataValidator.reset().parameter(LoanApiConstants.numberOfRepaymentsParameterName).value(numberOfRepayments).notNull() + .integerGreaterThanZero(); + } + + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.repaymentEveryParameterName, element)) { + atLeastOneParameterPassedForUpdate = true; + final Integer repaymentEvery = this.fromApiJsonHelper + .extractIntegerWithLocaleNamed(LoanApiConstants.repaymentEveryParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.repaymentEveryParameterName).value(repaymentEvery).notNull() + .integerGreaterThanZero(); + } + + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.repaymentFrequencyTypeParameterName, element)) { + atLeastOneParameterPassedForUpdate = true; + final Integer repaymentEveryType = this.fromApiJsonHelper + .extractIntegerWithLocaleNamed(LoanApiConstants.repaymentFrequencyTypeParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.repaymentFrequencyTypeParameterName).value(repaymentEveryType) + .notNull().inMinMaxRange(0, 3); + } + + CalendarUtils.validateNthDayOfMonthFrequency(baseDataValidator, LoanApiConstants.repaymentFrequencyNthDayTypeParameterName, + LoanApiConstants.repaymentFrequencyDayOfWeekTypeParameterName, element, this.fromApiJsonHelper); + + Integer interestType = null; + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.interestTypeParameterName, element)) { + atLeastOneParameterPassedForUpdate = true; + interestType = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(LoanApiConstants.interestTypeParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.interestTypeParameterName).value(interestType).notNull() + .inMinMaxRange(0, 1); + } + + if (loanProduct.isLinkedToFloatingInterestRate()) { + if (isEqualAmortization) { + throw new EqualAmortizationUnsupportedFeatureException("floating.interest.rate", "floating interest rate"); + } + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.interestRatePerPeriodParameterName, element)) { + baseDataValidator.reset().parameter(LoanApiConstants.interestRatePerPeriodParameterName).failWithCode( + "not.supported.loanproduct.linked.to.floating.rate", + "interestRatePerPeriod param is not supported, selected Loan Product is linked with floating interest rate."); + } + + Boolean isFloatingInterestRate = loan.getIsFloatingInterestRate(); + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.isFloatingInterestRate, element)) { + isFloatingInterestRate = this.fromApiJsonHelper.extractBooleanNamed(LoanApiConstants.isFloatingInterestRate, element); + atLeastOneParameterPassedForUpdate = true; + } + if (isFloatingInterestRate != null) { + if (isFloatingInterestRate && !loanProduct.getFloatingRates().isFloatingInterestRateCalculationAllowed()) { + baseDataValidator.reset().parameter(LoanApiConstants.isFloatingInterestRate).failWithCode( + "true.not.supported.for.selected.loanproduct", + "isFloatingInterestRate value of true not supported for selected Loan Product."); + } + } else { + baseDataValidator.reset().parameter(LoanApiConstants.isFloatingInterestRate).trueOrFalseRequired(false); + } + + if (interestType == null) { + interestType = loan.getLoanProductRelatedDetail().getInterestMethod().getValue(); + } + if (interestType != null && interestType.equals(InterestMethod.FLAT.getValue())) { + baseDataValidator.reset().parameter(LoanApiConstants.interestTypeParameterName).failWithCode( + "should.be.0.for.selected.loan.product", + "interestType should be DECLINING_BALANCE for selected Loan Product as it is linked to floating rates."); + } + + BigDecimal interestRateDifferential = loan.getInterestRateDifferential(); + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.interestRateDifferentialParameterName, element)) { + interestRateDifferential = this.fromApiJsonHelper + .extractBigDecimalWithLocaleNamed(LoanApiConstants.interestRateDifferentialParameterName, element); + atLeastOneParameterPassedForUpdate = true; + } + baseDataValidator.reset().parameter(LoanApiConstants.interestRateDifferentialParameterName).value(interestRateDifferential) + .notNull().zeroOrPositiveAmount() + .inMinAndMaxAmountRange(loanProduct.getFloatingRates().getMinDifferentialLendingRate(), + loanProduct.getFloatingRates().getMaxDifferentialLendingRate()); + } else { - baseDataValidator.reset().parameter(LoanApiConstants.isFloatingInterestRate).trueOrFalseRequired(false); + + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.isFloatingInterestRate, element)) { + baseDataValidator.reset().parameter(LoanApiConstants.isFloatingInterestRate).failWithCode( + "not.supported.loanproduct.not.linked.to.floating.rate", + "isFloatingInterestRate param is not supported, selected Loan Product is not linked with floating interest rate."); + } + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.interestRateDifferential, element)) { + baseDataValidator.reset().parameter(LoanApiConstants.interestRateDifferential).failWithCode( + "not.supported.loanproduct.not.linked.to.floating.rate", + "interestRateDifferential param is not supported, selected Loan Product is not linked with floating interest rate."); + } + + BigDecimal interestRatePerPeriod = loan.getLoanProductRelatedDetail().getNominalInterestRatePerPeriod(); + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.interestRatePerPeriodParameterName, element)) { + this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.interestRatePerPeriodParameterName, element); + atLeastOneParameterPassedForUpdate = true; + } + baseDataValidator.reset().parameter(LoanApiConstants.interestRatePerPeriodParameterName).value(interestRatePerPeriod) + .notNull().zeroOrPositiveAmount(); + } - if (interestType == null) { - interestType = loan.getLoanProductRelatedDetail().getInterestMethod().getValue(); - } - if (interestType != null && interestType.equals(InterestMethod.FLAT.getValue())) { - baseDataValidator.reset().parameter(LoanApiConstants.interestTypeParameterName).failWithCode( - "should.be.0.for.selected.loan.product", - "interestType should be DECLINING_BALANCE for selected Loan Product as it is linked to floating rates."); - } + Integer interestCalculationPeriodType = loanProduct.getLoanProductRelatedDetail().getInterestCalculationPeriodMethod() + .getValue(); - BigDecimal interestRateDifferential = loan.getInterestRateDifferential(); - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.interestRateDifferentialParameterName, element)) { - interestRateDifferential = this.fromApiJsonHelper - .extractBigDecimalWithLocaleNamed(LoanApiConstants.interestRateDifferentialParameterName, element); + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.interestCalculationPeriodTypeParameterName, element)) { atLeastOneParameterPassedForUpdate = true; - } - baseDataValidator.reset().parameter(LoanApiConstants.interestRateDifferentialParameterName).value(interestRateDifferential) - .notNull().zeroOrPositiveAmount().inMinAndMaxAmountRange(loanProduct.getFloatingRates().getMinDifferentialLendingRate(), - loanProduct.getFloatingRates().getMaxDifferentialLendingRate()); - - } else { - - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.isFloatingInterestRate, element)) { - baseDataValidator.reset().parameter(LoanApiConstants.isFloatingInterestRate).failWithCode( - "not.supported.loanproduct.not.linked.to.floating.rate", - "isFloatingInterestRate param is not supported, selected Loan Product is not linked with floating interest rate."); - } - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.interestRateDifferential, element)) { - baseDataValidator.reset().parameter(LoanApiConstants.interestRateDifferential).failWithCode( - "not.supported.loanproduct.not.linked.to.floating.rate", - "interestRateDifferential param is not supported, selected Loan Product is not linked with floating interest rate."); + interestCalculationPeriodType = this.fromApiJsonHelper + .extractIntegerWithLocaleNamed(LoanApiConstants.interestCalculationPeriodTypeParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.interestCalculationPeriodTypeParameterName) + .value(interestCalculationPeriodType).notNull().inMinMaxRange(0, 1); } - BigDecimal interestRatePerPeriod = loan.getLoanProductRelatedDetail().getNominalInterestRatePerPeriod(); - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.interestRatePerPeriodParameterName, element)) { - this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.interestRatePerPeriodParameterName, element); + Integer amortizationType = null; + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.amortizationTypeParameterName, element)) { atLeastOneParameterPassedForUpdate = true; - } - baseDataValidator.reset().parameter(LoanApiConstants.interestRatePerPeriodParameterName).value(interestRatePerPeriod).notNull() - .zeroOrPositiveAmount(); - - } - - Integer interestCalculationPeriodType = loanProduct.getLoanProductRelatedDetail().getInterestCalculationPeriodMethod().getValue(); - - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.interestCalculationPeriodTypeParameterName, element)) { - atLeastOneParameterPassedForUpdate = true; - interestCalculationPeriodType = this.fromApiJsonHelper - .extractIntegerWithLocaleNamed(LoanApiConstants.interestCalculationPeriodTypeParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.interestCalculationPeriodTypeParameterName) - .value(interestCalculationPeriodType).notNull().inMinMaxRange(0, 1); - } - - Integer amortizationType = null; - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.amortizationTypeParameterName, element)) { - atLeastOneParameterPassedForUpdate = true; - amortizationType = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(LoanApiConstants.amortizationTypeParameterName, - element); - baseDataValidator.reset().parameter(LoanApiConstants.amortizationTypeParameterName).value(amortizationType).notNull() - .inMinMaxRange(0, 1); - } - - if (!AmortizationMethod.EQUAL_PRINCIPAL.getValue().equals(amortizationType) && fixedPrincipalPercentagePerInstallment != null) { - baseDataValidator.reset().parameter(LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName).failWithCode( - "not.supported.principal.fixing.not.allowed.with.equal.installments", - "Principal fixing cannot be done with equal installment amortization"); - } - - LocalDate expectedDisbursementDate = loan.getExpectedDisbursementDate(); - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.expectedDisbursementDateParameterName, element)) { - atLeastOneParameterPassedForUpdate = true; - - final String expectedDisbursementDateStr = this.fromApiJsonHelper - .extractStringNamed(LoanApiConstants.expectedDisbursementDateParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.expectedDisbursementDateParameterName).value(expectedDisbursementDateStr) - .notBlank(); - - expectedDisbursementDate = this.fromApiJsonHelper.extractLocalDateNamed(LoanApiConstants.expectedDisbursementDateParameterName, - element); - baseDataValidator.reset().parameter(LoanApiConstants.expectedDisbursementDateParameterName).value(expectedDisbursementDate) - .notNull(); - } - - Integer graceOnPrincipalPayment = loan.getLoanProductRelatedDetail().getGraceOnPrincipalPayment(); - // grace validation - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.graceOnPrincipalPaymentParameterName, element)) { - graceOnPrincipalPayment = this.fromApiJsonHelper - .extractIntegerWithLocaleNamed(LoanApiConstants.graceOnPrincipalPaymentParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.graceOnPrincipalPaymentParameterName).value(graceOnPrincipalPayment) - .zeroOrPositiveAmount(); - } - - Integer graceOnInterestPayment = loan.getLoanProductRelatedDetail().getGraceOnInterestPayment(); - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.graceOnInterestPaymentParameterName, element)) { - graceOnInterestPayment = this.fromApiJsonHelper - .extractIntegerWithLocaleNamed(LoanApiConstants.graceOnInterestPaymentParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.graceOnInterestPaymentParameterName).value(graceOnInterestPayment) - .zeroOrPositiveAmount(); - } - - Integer graceOnInterestCharged = loan.getLoanProductRelatedDetail().getGraceOnInterestCharged(); - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.graceOnInterestChargedParameterName, element)) { - graceOnInterestCharged = this.fromApiJsonHelper - .extractIntegerWithLocaleNamed(LoanApiConstants.graceOnInterestChargedParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.graceOnInterestChargedParameterName).value(graceOnInterestCharged) - .zeroOrPositiveAmount(); - } - - if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.GRACE_ON_ARREARS_AGEING_PARAMETER_NAME, element)) { - final Integer graceOnArrearsAgeing = this.fromApiJsonHelper - .extractIntegerWithLocaleNamed(LoanProductConstants.GRACE_ON_ARREARS_AGEING_PARAMETER_NAME, element); - baseDataValidator.reset().parameter(LoanProductConstants.GRACE_ON_ARREARS_AGEING_PARAMETER_NAME).value(graceOnArrearsAgeing) - .zeroOrPositiveAmount(); - } - - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.interestChargedFromDateParameterName, element)) { - atLeastOneParameterPassedForUpdate = true; - final LocalDate interestChargedFromDate = this.fromApiJsonHelper - .extractLocalDateNamed(LoanApiConstants.interestChargedFromDateParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.interestChargedFromDateParameterName).value(interestChargedFromDate) - .ignoreIfNull(); - } - - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.repaymentsStartingFromDateParameterName, element)) { - atLeastOneParameterPassedForUpdate = true; - final LocalDate repaymentsStartingFromDate = this.fromApiJsonHelper - .extractLocalDateNamed(LoanApiConstants.repaymentsStartingFromDateParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.repaymentsStartingFromDateParameterName).value(repaymentsStartingFromDate) - .ignoreIfNull(); - if (!loan.getLoanTermVariations().isEmpty()) { - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("cannot.modify.application.due.to.variable.installments"); - } - } - - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.submittedOnDateParameterName, element)) { - atLeastOneParameterPassedForUpdate = true; - final LocalDate submittedOnDate = this.fromApiJsonHelper.extractLocalDateNamed(LoanApiConstants.submittedOnDateParameterName, - element); - baseDataValidator.reset().parameter(LoanApiConstants.submittedOnDateParameterName).value(submittedOnDate).notNull(); - } - - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.submittedOnNoteParameterName, element)) { - atLeastOneParameterPassedForUpdate = true; - final String submittedOnNote = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.submittedOnNoteParameterName, - element); - baseDataValidator.reset().parameter(LoanApiConstants.submittedOnNoteParameterName).value(submittedOnNote).ignoreIfNull() - .notExceedingLengthOf(500); - } - - validateLinkedSavingsAccount(element, baseDataValidator); - - // charges - loanChargeApiJsonValidator.validateLoanCharges(element, loanProduct, baseDataValidator); - - final String loanTypeStr = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.loanTypeParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.loanTypeParameterName).value(loanTypeStr).notNull(); - - if (!StringUtils.isBlank(loanTypeStr)) { - final AccountType loanType = AccountType.fromName(loanTypeStr); - - if (loanType.isInvalid()) { - baseDataValidator.reset().parameter(LoanApiConstants.loanTypeParameterName).value(loanType.getValue()) - .isOneOfEnumValues(AccountType.class); + amortizationType = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(LoanApiConstants.amortizationTypeParameterName, + element); + baseDataValidator.reset().parameter(LoanApiConstants.amortizationTypeParameterName).value(amortizationType).notNull() + .inMinMaxRange(0, 1); } - if (!loanType.isInvalid() && loanType.isIndividualAccount()) { - // collateral - final String collateralParameterName = LoanApiConstants.collateralParameterName; - if (element.isJsonObject() && this.fromApiJsonHelper.parameterExists(collateralParameterName, element)) { - final JsonObject topLevelJsonElement = element.getAsJsonObject(); - final Locale locale = this.fromApiJsonHelper.extractLocaleParameter(topLevelJsonElement); - if (topLevelJsonElement.get(LoanApiConstants.collateralParameterName).isJsonArray()) { + if (!AmortizationMethod.EQUAL_PRINCIPAL.getValue().equals(amortizationType) && fixedPrincipalPercentagePerInstallment != null) { + baseDataValidator.reset().parameter(LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName).failWithCode( + "not.supported.principal.fixing.not.allowed.with.equal.installments", + "Principal fixing cannot be done with equal installment amortization"); + } - final Type collateralParameterTypeOfMap = new TypeToken>() { + LocalDate expectedDisbursementDate = loan.getExpectedDisbursementDate(); + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.expectedDisbursementDateParameterName, element)) { + atLeastOneParameterPassedForUpdate = true; - }.getType(); - final Set supportedParameters = new HashSet<>(Arrays.asList(LoanApiConstants.idParameterName, - LoanApiConstants.clientCollateralIdParameterName, LoanApiConstants.quantityParameterName)); - final JsonArray array = topLevelJsonElement.get(LoanApiConstants.collateralParameterName).getAsJsonArray(); - if (array.size() > 0) { - BigDecimal totalAmount = BigDecimal.ZERO; - for (int i = 1; i <= array.size(); i++) { - final JsonObject collateralItemElement = array.get(i - 1).getAsJsonObject(); + final String expectedDisbursementDateStr = this.fromApiJsonHelper + .extractStringNamed(LoanApiConstants.expectedDisbursementDateParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.expectedDisbursementDateParameterName) + .value(expectedDisbursementDateStr).notBlank(); - final String collateralJson = this.fromApiJsonHelper.toJson(collateralItemElement); - this.fromApiJsonHelper.checkForUnsupportedParameters(collateralParameterTypeOfMap, collateralJson, - supportedParameters); + expectedDisbursementDate = this.fromApiJsonHelper + .extractLocalDateNamed(LoanApiConstants.expectedDisbursementDateParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.expectedDisbursementDateParameterName).value(expectedDisbursementDate) + .notNull(); + } - final Long id = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.idParameterName, - collateralItemElement); - baseDataValidator.reset().parameter(LoanApiConstants.collateralParameterName) - .parameterAtIndexArray(LoanApiConstants.idParameterName, i).value(id).ignoreIfNull(); + Integer graceOnPrincipalPayment = loan.getLoanProductRelatedDetail().getGraceOnPrincipalPayment(); + // grace validation + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.graceOnPrincipalPaymentParameterName, element)) { + graceOnPrincipalPayment = this.fromApiJsonHelper + .extractIntegerWithLocaleNamed(LoanApiConstants.graceOnPrincipalPaymentParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.graceOnPrincipalPaymentParameterName).value(graceOnPrincipalPayment) + .zeroOrPositiveAmount(); + } - final Long clientCollateralId = this.fromApiJsonHelper - .extractLongNamed(LoanApiConstants.clientCollateralIdParameterName, collateralItemElement); - baseDataValidator.reset().parameter(LoanApiConstants.collateralParameterName) - .parameterAtIndexArray(LoanApiConstants.clientCollateralIdParameterName, i) - .value(clientCollateralId).notNull().integerGreaterThanZero(); + Integer graceOnInterestPayment = loan.getLoanProductRelatedDetail().getGraceOnInterestPayment(); + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.graceOnInterestPaymentParameterName, element)) { + graceOnInterestPayment = this.fromApiJsonHelper + .extractIntegerWithLocaleNamed(LoanApiConstants.graceOnInterestPaymentParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.graceOnInterestPaymentParameterName).value(graceOnInterestPayment) + .zeroOrPositiveAmount(); + } - final BigDecimal quantity = this.fromApiJsonHelper - .extractBigDecimalNamed(LoanApiConstants.quantityParameterName, collateralItemElement, locale); - baseDataValidator.reset().parameter(LoanApiConstants.collateralParameterName) - .parameterAtIndexArray(LoanApiConstants.quantityParameterName, i).value(quantity).notNull() - .positiveAmount(); + Integer graceOnInterestCharged = loan.getLoanProductRelatedDetail().getGraceOnInterestCharged(); + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.graceOnInterestChargedParameterName, element)) { + graceOnInterestCharged = this.fromApiJsonHelper + .extractIntegerWithLocaleNamed(LoanApiConstants.graceOnInterestChargedParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.graceOnInterestChargedParameterName).value(graceOnInterestCharged) + .zeroOrPositiveAmount(); + } - if (clientCollateralId != null || quantity != null) { - BigDecimal baseAmount = this.clientCollateralManagementRepositoryWrapper - .getCollateral(clientCollateralId).getCollaterals().getBasePrice(); - BigDecimal pctToBase = this.clientCollateralManagementRepositoryWrapper - .getCollateral(clientCollateralId).getCollaterals().getPctToBase(); - BigDecimal total = baseAmount.multiply(pctToBase).multiply(quantity); - totalAmount = totalAmount.add(total); + if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.GRACE_ON_ARREARS_AGEING_PARAMETER_NAME, element)) { + final Integer graceOnArrearsAgeing = this.fromApiJsonHelper + .extractIntegerWithLocaleNamed(LoanProductConstants.GRACE_ON_ARREARS_AGEING_PARAMETER_NAME, element); + baseDataValidator.reset().parameter(LoanProductConstants.GRACE_ON_ARREARS_AGEING_PARAMETER_NAME).value(graceOnArrearsAgeing) + .zeroOrPositiveAmount(); + } + + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.interestChargedFromDateParameterName, element)) { + atLeastOneParameterPassedForUpdate = true; + final LocalDate interestChargedFromDate = this.fromApiJsonHelper + .extractLocalDateNamed(LoanApiConstants.interestChargedFromDateParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.interestChargedFromDateParameterName).value(interestChargedFromDate) + .ignoreIfNull(); + } + + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.repaymentsStartingFromDateParameterName, element)) { + atLeastOneParameterPassedForUpdate = true; + final LocalDate repaymentsStartingFromDate = this.fromApiJsonHelper + .extractLocalDateNamed(LoanApiConstants.repaymentsStartingFromDateParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.repaymentsStartingFromDateParameterName) + .value(repaymentsStartingFromDate).ignoreIfNull(); + if (!loan.getLoanTermVariations().isEmpty()) { + baseDataValidator.reset() + .failWithCodeNoParameterAddedToErrorCode("cannot.modify.application.due.to.variable.installments"); + } + } + + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.submittedOnDateParameterName, element)) { + atLeastOneParameterPassedForUpdate = true; + final LocalDate submittedOnDate = this.fromApiJsonHelper + .extractLocalDateNamed(LoanApiConstants.submittedOnDateParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.submittedOnDateParameterName).value(submittedOnDate).notNull(); + } + + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.submittedOnNoteParameterName, element)) { + atLeastOneParameterPassedForUpdate = true; + final String submittedOnNote = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.submittedOnNoteParameterName, + element); + baseDataValidator.reset().parameter(LoanApiConstants.submittedOnNoteParameterName).value(submittedOnNote).ignoreIfNull() + .notExceedingLengthOf(500); + } + + validateLinkedSavingsAccount(element, baseDataValidator); + + // charges + loanChargeApiJsonValidator.validateLoanCharges(element, loanProduct, baseDataValidator); + + final String loanTypeStr = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.loanTypeParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.loanTypeParameterName).value(loanTypeStr).notNull(); + + if (!StringUtils.isBlank(loanTypeStr)) { + final AccountType loanType = AccountType.fromName(loanTypeStr); + + if (loanType.isInvalid()) { + baseDataValidator.reset().parameter(LoanApiConstants.loanTypeParameterName).value(loanType.getValue()) + .isOneOfEnumValues(AccountType.class); + } + + if (!loanType.isInvalid() && loanType.isIndividualAccount()) { + // collateral + final String collateralParameterName = LoanApiConstants.collateralParameterName; + if (element.isJsonObject() && this.fromApiJsonHelper.parameterExists(collateralParameterName, element)) { + final JsonObject topLevelJsonElement = element.getAsJsonObject(); + final Locale locale = this.fromApiJsonHelper.extractLocaleParameter(topLevelJsonElement); + if (topLevelJsonElement.get(LoanApiConstants.collateralParameterName).isJsonArray()) { + + final Type collateralParameterTypeOfMap = new TypeToken>() { + + }.getType(); + final Set supportedParameters = new HashSet<>(Arrays.asList(LoanApiConstants.idParameterName, + LoanApiConstants.clientCollateralIdParameterName, LoanApiConstants.quantityParameterName)); + final JsonArray array = topLevelJsonElement.get(LoanApiConstants.collateralParameterName).getAsJsonArray(); + if (array.size() > 0) { + BigDecimal totalAmount = BigDecimal.ZERO; + for (int i = 1; i <= array.size(); i++) { + final JsonObject collateralItemElement = array.get(i - 1).getAsJsonObject(); + + final String collateralJson = this.fromApiJsonHelper.toJson(collateralItemElement); + this.fromApiJsonHelper.checkForUnsupportedParameters(collateralParameterTypeOfMap, collateralJson, + supportedParameters); + + final Long id = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.idParameterName, + collateralItemElement); + baseDataValidator.reset().parameter(LoanApiConstants.collateralParameterName) + .parameterAtIndexArray(LoanApiConstants.idParameterName, i).value(id).ignoreIfNull(); + + final Long clientCollateralId = this.fromApiJsonHelper + .extractLongNamed(LoanApiConstants.clientCollateralIdParameterName, collateralItemElement); + baseDataValidator.reset().parameter(LoanApiConstants.collateralParameterName) + .parameterAtIndexArray(LoanApiConstants.clientCollateralIdParameterName, i) + .value(clientCollateralId).notNull().integerGreaterThanZero(); + + final BigDecimal quantity = this.fromApiJsonHelper + .extractBigDecimalNamed(LoanApiConstants.quantityParameterName, collateralItemElement, locale); + baseDataValidator.reset().parameter(LoanApiConstants.collateralParameterName) + .parameterAtIndexArray(LoanApiConstants.quantityParameterName, i).value(quantity).notNull() + .positiveAmount(); + + if (clientCollateralId != null || quantity != null) { + BigDecimal baseAmount = this.clientCollateralManagementRepositoryWrapper + .getCollateral(clientCollateralId).getCollaterals().getBasePrice(); + BigDecimal pctToBase = this.clientCollateralManagementRepositoryWrapper + .getCollateral(clientCollateralId).getCollaterals().getPctToBase(); + BigDecimal total = baseAmount.multiply(pctToBase).multiply(quantity); + totalAmount = totalAmount.add(total); + } + } + if (principal != null && principal.compareTo(totalAmount) > 0) { + throw new InvalidAmountOfCollaterals(totalAmount); } } - if (principal != null && principal.compareTo(totalAmount) > 0) { - throw new InvalidAmountOfCollaterals(totalAmount); - } + } else { + baseDataValidator.reset().parameter(collateralParameterName).expectedArrayButIsNot(); } - } else { - baseDataValidator.reset().parameter(collateralParameterName).expectedArrayButIsNot(); } } } - } - boolean meetingIdRequired = false; - // validate syncDisbursement - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.syncDisbursementWithMeetingParameterName, element)) { - final Boolean syncDisbursement = this.fromApiJsonHelper - .extractBooleanNamed(LoanApiConstants.syncDisbursementWithMeetingParameterName, element); - if (syncDisbursement == null) { - baseDataValidator.reset().parameter(LoanApiConstants.syncDisbursementWithMeetingParameterName).value(syncDisbursement) - .trueOrFalseRequired(false); - } else if (syncDisbursement.booleanValue()) { - meetingIdRequired = true; + boolean meetingIdRequired = false; + // validate syncDisbursement + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.syncDisbursementWithMeetingParameterName, element)) { + final Boolean syncDisbursement = this.fromApiJsonHelper + .extractBooleanNamed(LoanApiConstants.syncDisbursementWithMeetingParameterName, element); + if (syncDisbursement == null) { + baseDataValidator.reset().parameter(LoanApiConstants.syncDisbursementWithMeetingParameterName).value(syncDisbursement) + .trueOrFalseRequired(false); + } else if (syncDisbursement.booleanValue()) { + meetingIdRequired = true; + } } - } - // if disbursement is synced then must have a meeting (calendar) - if (meetingIdRequired || this.fromApiJsonHelper.parameterExists(LoanApiConstants.calendarIdParameterName, element)) { - final Long calendarId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.calendarIdParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.calendarIdParameterName).value(calendarId).notNull() - .integerGreaterThanZero(); - } - - if (!atLeastOneParameterPassedForUpdate) { - final Object forceError = null; - baseDataValidator.reset().anyOfNotNull(forceError); - } - - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.fixedEmiAmountParameterName, element)) { - if (!(loanProduct.isCanDefineInstallmentAmount() || loanProduct.isMultiDisburseLoan())) { - List unsupportedParameterList = new ArrayList<>(); - unsupportedParameterList.add(LoanApiConstants.fixedEmiAmountParameterName); - throw new UnsupportedParameterException(unsupportedParameterList); + // if disbursement is synced then must have a meeting (calendar) + if (meetingIdRequired || this.fromApiJsonHelper.parameterExists(LoanApiConstants.calendarIdParameterName, element)) { + final Long calendarId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.calendarIdParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.calendarIdParameterName).value(calendarId).notNull() + .integerGreaterThanZero(); } - if (isEqualAmortization) { - throw new EqualAmortizationUnsupportedFeatureException("fixed.emi", "fixed emi"); + + if (!atLeastOneParameterPassedForUpdate) { + final Object forceError = null; + baseDataValidator.reset().anyOfNotNull(forceError); } - final BigDecimal emiAnount = this.fromApiJsonHelper - .extractBigDecimalWithLocaleNamed(LoanApiConstants.fixedEmiAmountParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.fixedEmiAmountParameterName).value(emiAnount).ignoreIfNull() - .positiveAmount(); - } - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.maxOutstandingBalanceParameterName, element)) { - final BigDecimal maxOutstandingBalance = this.fromApiJsonHelper - .extractBigDecimalWithLocaleNamed(LoanApiConstants.maxOutstandingBalanceParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.maxOutstandingBalanceParameterName).value(maxOutstandingBalance) - .ignoreIfNull().positiveAmount(); - } + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.fixedEmiAmountParameterName, element)) { + if (!(loanProduct.isCanDefineInstallmentAmount() || loanProduct.isMultiDisburseLoan())) { + List unsupportedParameterList = new ArrayList<>(); + unsupportedParameterList.add(LoanApiConstants.fixedEmiAmountParameterName); + throw new UnsupportedParameterException(unsupportedParameterList); + } + if (isEqualAmortization) { + throw new EqualAmortizationUnsupportedFeatureException("fixed.emi", "fixed emi"); + } + final BigDecimal emiAnount = this.fromApiJsonHelper + .extractBigDecimalWithLocaleNamed(LoanApiConstants.fixedEmiAmountParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.fixedEmiAmountParameterName).value(emiAnount).ignoreIfNull() + .positiveAmount(); + } - if (loanProduct.isCanUseForTopup() && this.fromApiJsonHelper.parameterExists(LoanApiConstants.isTopup, element)) { - final Boolean isTopup = this.fromApiJsonHelper.extractBooleanNamed(LoanApiConstants.isTopup, element); - baseDataValidator.reset().parameter(LoanApiConstants.isTopup).value(isTopup).ignoreIfNull().validateForBooleanValue(); + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.maxOutstandingBalanceParameterName, element)) { + final BigDecimal maxOutstandingBalance = this.fromApiJsonHelper + .extractBigDecimalWithLocaleNamed(LoanApiConstants.maxOutstandingBalanceParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.maxOutstandingBalanceParameterName).value(maxOutstandingBalance) + .ignoreIfNull().positiveAmount(); + } - if (isTopup != null && isTopup) { - final Long loanId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.loanIdToClose, element); - baseDataValidator.reset().parameter(LoanApiConstants.loanIdToClose).value(loanId).notNull().longGreaterThanZero(); + if (loanProduct.isCanUseForTopup() && this.fromApiJsonHelper.parameterExists(LoanApiConstants.isTopup, element)) { + final Boolean isTopup = this.fromApiJsonHelper.extractBooleanNamed(LoanApiConstants.isTopup, element); + baseDataValidator.reset().parameter(LoanApiConstants.isTopup).value(isTopup).ignoreIfNull().validateForBooleanValue(); - LocalDate submittedOnDate = this.fromApiJsonHelper.extractLocalDateNamed(LoanApiConstants.submittedOnDateParameterName, + if (isTopup != null && isTopup) { + final Long loanId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.loanIdToClose, element); + baseDataValidator.reset().parameter(LoanApiConstants.loanIdToClose).value(loanId).notNull().longGreaterThanZero(); + + LocalDate submittedOnDate = this.fromApiJsonHelper.extractLocalDateNamed(LoanApiConstants.submittedOnDateParameterName, + element); + if (submittedOnDate == null) { + submittedOnDate = loan.getSubmittedOnDate(); + } + final Long loanIdToClose = command.longValueOfParameterNamed(LoanApiConstants.loanIdToClose); + final Loan loanToClose = this.loanRepositoryWrapper.findNonClosedLoanThatBelongsToClient(loanIdToClose, clientId); + if (loanToClose == null) { + throw new GeneralPlatformDomainRuleException( + "error.msg.loan.loanIdToClose.no.active.loan.associated.to.client.found", + "loanIdToClose is invalid, No Active Loan associated with the given Client ID found."); + } + if (loanToClose.isMultiDisburmentLoan() && !loanToClose.isInterestRecalculationEnabledForProduct()) { + throw new GeneralPlatformDomainRuleException( + "error.msg.loan.topup.on.multi.tranche.loan.without.interest.recalculation.not.supported", + "Topup on loan with multi-tranche disbursal and without interest recalculation is not supported."); + } + final LocalDate disbursalDateOfLoanToClose = loanToClose.getDisbursementDate(); + if (!DateUtils.isAfter(submittedOnDate, disbursalDateOfLoanToClose)) { + throw new GeneralPlatformDomainRuleException( + "error.msg.loan.submitted.date.should.be.after.topup.loan.disbursal.date", + "Submitted date of this loan application " + submittedOnDate + + " should be after the disbursed date of loan to be closed " + disbursalDateOfLoanToClose); + } + if (!loanToClose.getCurrencyCode().equals(loanProduct.getCurrency().getCode())) { + throw new GeneralPlatformDomainRuleException("error.msg.loan.to.be.closed.has.different.currency", + "loanIdToClose is invalid, Currency code is different."); + } + final LocalDate lastUserTransactionOnLoanToClose = loanToClose.getLastUserTransactionDate(); + if (DateUtils.isBefore(expectedDisbursementDate, lastUserTransactionOnLoanToClose)) { + throw new GeneralPlatformDomainRuleException( + "error.msg.loan.disbursal.date.should.be.after.last.transaction.date.of.loan.to.be.closed", + "Disbursal date of this loan application " + expectedDisbursementDate + + " should be after last transaction date of loan to be closed " + + lastUserTransactionOnLoanToClose); + } + BigDecimal loanOutstanding = this.loanReadPlatformService + .retrieveLoanPrePaymentTemplate(LoanTransactionType.REPAYMENT, loanIdToClose, expectedDisbursementDate) + .getAmount(); + final BigDecimal firstDisbursalAmount = loan.getFirstDisbursalAmount(); + if (loanOutstanding.compareTo(firstDisbursalAmount) > 0) { + throw new GeneralPlatformDomainRuleException("error.msg.loan.amount.less.than.outstanding.of.loan.to.be.closed", + "Topup loan amount should be greater than outstanding amount of loan to be closed."); + } + + } + } + + validateLoanMultiDisbursementDate(element, baseDataValidator, expectedDisbursementDate, principal); + validatePartialPeriodSupport(interestCalculationPeriodType, baseDataValidator, element, loanProduct); + + String loanScheduleProcessingType = loan.getLoanRepaymentScheduleDetail().getLoanScheduleProcessingType().name(); + if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE, element)) { + loanScheduleProcessingType = this.fromApiJsonHelper.extractStringNamed(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE, element); - if (submittedOnDate == null) { - submittedOnDate = loan.getSubmittedOnDate(); - } - final Long loanIdToClose = command.longValueOfParameterNamed(LoanApiConstants.loanIdToClose); - final Loan loanToClose = this.loanRepositoryWrapper.findNonClosedLoanThatBelongsToClient(loanIdToClose, clientId); - if (loanToClose == null) { - throw new GeneralPlatformDomainRuleException("error.msg.loan.loanIdToClose.no.active.loan.associated.to.client.found", - "loanIdToClose is invalid, No Active Loan associated with the given Client ID found."); - } - if (loanToClose.isMultiDisburmentLoan() && !loanToClose.isInterestRecalculationEnabledForProduct()) { - throw new GeneralPlatformDomainRuleException( - "error.msg.loan.topup.on.multi.tranche.loan.without.interest.recalculation.not.supported", - "Topup on loan with multi-tranche disbursal and without interest recalculation is not supported."); - } - final LocalDate disbursalDateOfLoanToClose = loanToClose.getDisbursementDate(); - if (!DateUtils.isAfter(submittedOnDate, disbursalDateOfLoanToClose)) { - throw new GeneralPlatformDomainRuleException("error.msg.loan.submitted.date.should.be.after.topup.loan.disbursal.date", - "Submitted date of this loan application " + submittedOnDate - + " should be after the disbursed date of loan to be closed " + disbursalDateOfLoanToClose); - } - if (!loanToClose.getCurrencyCode().equals(loanProduct.getCurrency().getCode())) { - throw new GeneralPlatformDomainRuleException("error.msg.loan.to.be.closed.has.different.currency", - "loanIdToClose is invalid, Currency code is different."); - } - final LocalDate lastUserTransactionOnLoanToClose = loanToClose.getLastUserTransactionDate(); - if (DateUtils.isBefore(expectedDisbursementDate, lastUserTransactionOnLoanToClose)) { - throw new GeneralPlatformDomainRuleException( - "error.msg.loan.disbursal.date.should.be.after.last.transaction.date.of.loan.to.be.closed", - "Disbursal date of this loan application " + expectedDisbursementDate - + " should be after last transaction date of loan to be closed " + lastUserTransactionOnLoanToClose); - } - BigDecimal loanOutstanding = this.loanReadPlatformService - .retrieveLoanPrePaymentTemplate(LoanTransactionType.REPAYMENT, loanIdToClose, expectedDisbursementDate).getAmount(); - final BigDecimal firstDisbursalAmount = loan.getFirstDisbursalAmount(); - if (loanOutstanding.compareTo(firstDisbursalAmount) > 0) { - throw new GeneralPlatformDomainRuleException("error.msg.loan.amount.less.than.outstanding.of.loan.to.be.closed", - "Topup loan amount should be greater than outstanding amount of loan to be closed."); - } - + baseDataValidator.reset().parameter(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE).value(loanScheduleProcessingType) + .ignoreIfNull().isOneOfEnumValues(LoanScheduleProcessingType.class); + } + if (LoanScheduleProcessingType.VERTICAL.equals(LoanScheduleProcessingType.valueOf(loanScheduleProcessingType)) + && !AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY + .equals(transactionProcessingStrategy)) { + baseDataValidator.reset().parameter(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE).failWithCode( + "supported.only.with.advanced.payment.allocation.strategy", + "Vertical repayment schedule processing is only available with `Advanced payment allocation` strategy"); } - } - validateLoanMultiDisbursementDate(element, baseDataValidator, expectedDisbursementDate, principal); - validatePartialPeriodSupport(interestCalculationPeriodType, baseDataValidator, element, loanProduct); + List allocationRules = loanProduct.getPaymentAllocationRules(); - String loanScheduleProcessingType = loan.getLoanRepaymentScheduleDetail().getLoanScheduleProcessingType().name(); - if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE, element)) { - loanScheduleProcessingType = this.fromApiJsonHelper.extractStringNamed(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE, - element); - baseDataValidator.reset().parameter(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE).value(loanScheduleProcessingType) - .ignoreIfNull().isOneOfEnumValues(LoanScheduleProcessingType.class); - } - if (LoanScheduleProcessingType.VERTICAL.equals(LoanScheduleProcessingType.valueOf(loanScheduleProcessingType)) - && !AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY - .equals(transactionProcessingStrategy)) { - baseDataValidator.reset().parameter(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE).failWithCode( - "supported.only.with.advanced.payment.allocation.strategy", - "Vertical repayment schedule processing is only available with `Advanced payment allocation` strategy"); - } + if (LoanScheduleProcessingType.HORIZONTAL.name().equals(loanScheduleProcessingType) + && AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY + .equals(transactionProcessingStrategy)) { + advancedPaymentAllocationsValidator.checkGroupingOfAllocationRules(allocationRules); + } - List allocationRules = loanProduct.getPaymentAllocationRules(); - - if (LoanScheduleProcessingType.HORIZONTAL.name().equals(loanScheduleProcessingType) - && AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY.equals(transactionProcessingStrategy)) { - advancedPaymentAllocationsValidator.checkGroupingOfAllocationRules(allocationRules); - } - - if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY, element)) { - final Boolean isEnableInstallmentLevelDelinquency = this.fromApiJsonHelper - .extractBooleanNamed(LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY, element); - baseDataValidator.reset().parameter(LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY) - .value(isEnableInstallmentLevelDelinquency).validateForBooleanValue(); - if (loanProduct.getDelinquencyBucket() == null) { - if (isEnableInstallmentLevelDelinquency) { - baseDataValidator.reset().parameter(LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY).failWithCode( - "can.be.enabled.for.loan.with.loan.product.having.valid.delinquency.bucket", - "Installment level delinquency cannot be enabled for a loan if Delinquency bucket is not configured for loan product"); + if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY, element)) { + final Boolean isEnableInstallmentLevelDelinquency = this.fromApiJsonHelper + .extractBooleanNamed(LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY, element); + baseDataValidator.reset().parameter(LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY) + .value(isEnableInstallmentLevelDelinquency).validateForBooleanValue(); + if (loanProduct.getDelinquencyBucket() == null) { + if (isEnableInstallmentLevelDelinquency) { + baseDataValidator.reset().parameter(LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY).failWithCode( + "can.be.enabled.for.loan.with.loan.product.having.valid.delinquency.bucket", + "Installment level delinquency cannot be enabled for a loan if Delinquency bucket is not configured for loan product"); + } } } - } - validateDisbursementDetails(loanProduct, element); - validateSubmittedOnDate(element, loan.getSubmittedOnDate(), loanProduct); + validateDisbursementDetails(loanProduct, element); + validateSubmittedOnDate(element, loan.getSubmittedOnDate(), loanProduct); - validateClientOrGroup(client, group, productId); + validateClientOrGroup(client, group, productId); - // validate if disbursement date is a holiday or a non-working day - validateDisbursementDateIsOnNonWorkingDay(expectedDisbursementDate); - Long officeId = client != null ? client.getOffice().getId() : group.getOffice().getId(); - validateDisbursementDateIsOnHoliday(expectedDisbursementDate, officeId); + // validate if disbursement date is a holiday or a non-working day + validateDisbursementDateIsOnNonWorkingDay(expectedDisbursementDate); + Long officeId = client != null ? client.getOffice().getId() : group.getOffice().getId(); + validateDisbursementDateIsOnHoliday(expectedDisbursementDate, officeId); - Integer recurringMoratoriumOnPrincipalPeriods = loan.getLoanProductRelatedDetail().getRecurringMoratoriumOnPrincipalPeriods(); - if (this.fromApiJsonHelper.parameterExists("recurringMoratoriumOnPrincipalPeriods", element)) { - recurringMoratoriumOnPrincipalPeriods = this.fromApiJsonHelper - .extractIntegerWithLocaleNamed("recurringMoratoriumOnPrincipalPeriods", element); - } - validateBorrowerCycle(element, loanProduct, clientId, groupId, baseDataValidator); + Integer recurringMoratoriumOnPrincipalPeriods = loan.getLoanProductRelatedDetail().getRecurringMoratoriumOnPrincipalPeriods(); + if (this.fromApiJsonHelper.parameterExists("recurringMoratoriumOnPrincipalPeriods", element)) { + recurringMoratoriumOnPrincipalPeriods = this.fromApiJsonHelper + .extractIntegerWithLocaleNamed("recurringMoratoriumOnPrincipalPeriods", element); + } + validateBorrowerCycle(element, loanProduct, clientId, groupId, baseDataValidator); - loanProductDataValidator.validateRepaymentPeriodWithGraceSettings(numberOfRepayments, graceOnPrincipalPayment, - graceOnInterestPayment, graceOnInterestCharged, recurringMoratoriumOnPrincipalPeriods, baseDataValidator); - - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", - dataValidationErrors); - } + loanProductDataValidator.validateRepaymentPeriodWithGraceSettings(numberOfRepayments, graceOnPrincipalPayment, + graceOnInterestPayment, graceOnInterestCharged, recurringMoratoriumOnPrincipalPeriods, baseDataValidator); + }); } private void validateClientOrGroup(Client client, Group group, Long productId) { @@ -1448,40 +1455,29 @@ public final class LoanApplicationValidator { }.getType(); this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, undoSupportedParameters); - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource(LOANAPPLICATION_UNDO); - final JsonElement element = this.fromApiJsonHelper.parse(json); + validateOrThrow(LOANAPPLICATION_UNDO, baseDataValidator -> { + final JsonElement element = this.fromApiJsonHelper.parse(json); - final String note = "note"; - if (this.fromApiJsonHelper.parameterExists(note, element)) { - final String noteText = this.fromApiJsonHelper.extractStringNamed(note, element); - baseDataValidator.reset().parameter(note).value(noteText).notExceedingLengthOf(1000); - } - - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } + final String note = "note"; + if (this.fromApiJsonHelper.parameterExists(note, element)) { + final String noteText = this.fromApiJsonHelper.extractStringNamed(note, element); + baseDataValidator.reset().parameter(note).value(noteText).notExceedingLengthOf(1000); + } + }); } public void validateMinMaxConstraintValues(final JsonElement element, final LoanProduct loanProduct) { + validateOrThrow("loan", baseDataValidator -> { + final BigDecimal minPrincipal = loanProduct.getMinPrincipalAmount().getAmount(); + final BigDecimal maxPrincipal = loanProduct.getMaxPrincipalAmount().getAmount(); + final String principalParameterName = LoanApiConstants.principalParameterName; - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan"); - - final BigDecimal minPrincipal = loanProduct.getMinPrincipalAmount().getAmount(); - final BigDecimal maxPrincipal = loanProduct.getMaxPrincipalAmount().getAmount(); - final String principalParameterName = LoanApiConstants.principalParameterName; - - if (this.fromApiJsonHelper.parameterExists(principalParameterName, element)) { - final BigDecimal principal = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(principalParameterName, element); - baseDataValidator.reset().parameter(principalParameterName).value(principal).notNull().positiveAmount() - .inMinAndMaxAmountRange(minPrincipal, maxPrincipal); - } - - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", - dataValidationErrors); - } + if (this.fromApiJsonHelper.parameterExists(principalParameterName, element)) { + final BigDecimal principal = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(principalParameterName, element); + baseDataValidator.reset().parameter(principalParameterName).value(principal).notNull().positiveAmount() + .inMinAndMaxAmountRange(minPrincipal, maxPrincipal); + } + }); } private void validateLoanTermAndRepaidEveryValues(final Integer loanTermFrequency, final Integer loanTermFrequencyType, @@ -1490,10 +1486,10 @@ public final class LoanApplicationValidator { this.loanScheduleValidator.validateSelectedPeriodFrequencyTypeIsTheSame(dataValidationErrors, loanTermFrequency, loanTermFrequencyType, numberOfRepayments, repaymentEvery, repaymentEveryType); - /** + /* * For multi-disbursal loans where schedules are auto-generated based on a fixed EMI, ensure the number of * repayments is within the permissible range defined by the loan product - **/ + */ // TODO: is this condition necessary? if (loan.getFixedEmiAmount() != null) { Integer minimumNoOfRepayments = loan.loanProduct().getMinNumberOfRepayments(); @@ -1583,6 +1579,12 @@ public final class LoanApplicationValidator { } } + public void validateLoanMultiDisbursementDate(final JsonElement element, LocalDate expectedDisbursementDate, BigDecimal principal) { + validateOrThrow("loan", baseDataValidator -> { + validateLoanMultiDisbursementDate(element, baseDataValidator, expectedDisbursementDate, principal); + }); + } + public void validateLoanMultiDisbursementDate(final JsonElement element, final DataValidatorBuilder baseDataValidator, LocalDate expectedDisbursement, BigDecimal totalPrincipal) { this.validateDisbursementsAreDatewiseOrdered(element, baseDataValidator); @@ -1648,23 +1650,16 @@ public final class LoanApplicationValidator { .integerSameAsNumber(InterestMethod.DECLINING_BALANCE.getValue()); } - } - } public void validateLoanForCollaterals(final Loan loan, final BigDecimal total) { - String errorCode; - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan"); - if (loan.getProposedPrincipal().compareTo(total) >= 0) { - errorCode = LoanApiConstants.LOAN_COLLATERAL_TOTAL_VALUE_SHOULD_BE_SUFFICIENT; - baseDataValidator.reset().parameter(LoanApiConstants.collateralsParameterName).failWithCode(errorCode); - } - - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } + validateOrThrow("loan", baseDataValidator -> { + if (loan.getProposedPrincipal().compareTo(total) >= 0) { + String errorCode = LoanApiConstants.LOAN_COLLATERAL_TOTAL_VALUE_SHOULD_BE_SUFFICIENT; + baseDataValidator.reset().parameter(LoanApiConstants.collateralsParameterName).failWithCode(errorCode); + } + }); } private void validatePartialPeriodSupport(final Integer interestCalculationPeriodType, final DataValidatorBuilder baseDataValidator, @@ -1876,4 +1871,44 @@ public final class LoanApplicationValidator { }.getType(); this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, SUPPORTED_PARAMETERS); } + + public void validateTopupLoan(Loan loan, LocalDate expectedDisbursementDate) { + final Long loanIdToClose = loan.getTopupLoanDetails().getLoanIdToClose(); + final Loan loanToClose = loanRepositoryWrapper.findNonClosedLoanThatBelongsToClient(loanIdToClose, loan.getClientId()); + if (loanToClose == null) { + throw new GeneralPlatformDomainRuleException("error.msg.loan.to.be.closed.with.topup.is.not.active", + "Loan to be closed with this topup is not active."); + } + + final LocalDate lastUserTransactionOnLoanToClose = loanToClose.getLastUserTransactionDate(); + if (DateUtils.isBefore(loan.getDisbursementDate(), lastUserTransactionOnLoanToClose)) { + throw new GeneralPlatformDomainRuleException( + "error.msg.loan.disbursal.date.should.be.after.last.transaction.date.of.loan.to.be.closed", + "Disbursal date of this loan application " + loan.getDisbursementDate() + + " should be after last transaction date of loan to be closed " + lastUserTransactionOnLoanToClose); + } + + BigDecimal loanOutstanding = loanReadPlatformService + .retrieveLoanPrePaymentTemplate(LoanTransactionType.REPAYMENT, loanIdToClose, expectedDisbursementDate).getAmount(); + final BigDecimal firstDisbursalAmount = loan.getFirstDisbursalAmount(); + if (loanOutstanding.compareTo(firstDisbursalAmount) > 0) { + throw new GeneralPlatformDomainRuleException("error.msg.loan.amount.less.than.outstanding.of.loan.to.be.closed", + "Topup loan amount should be greater than outstanding amount of loan to be closed."); + } + + BigDecimal netDisbursalAmount = loan.getApprovedPrincipal().subtract(loanOutstanding); + loan.adjustNetDisbursalAmount(netDisbursalAmount); + } + + public static void validateOrThrow(String resource, Consumer baseDataValidator) { + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder dataValidatorBuilder = new DataValidatorBuilder(dataValidationErrors).resource(resource); + + baseDataValidator.accept(dataValidatorBuilder); + + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", + dataValidationErrors); + } + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java index 995a1b5c4..e7a2f6bda 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java @@ -18,6 +18,8 @@ */ package org.apache.fineract.portfolio.loanaccount.service; +import static org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType.SAME_AS_REPAYMENT_PERIOD; + import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; @@ -236,28 +238,21 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa private void createCalendar(final Loan loan, LocalDate calendarStartDate, Integer recalculationFrequencyNthDay, final Integer repeatsOnDay, final RecalculationFrequencyType recalculationFrequencyType, Integer frequency, CalendarEntityType calendarEntityType, final String title) { - CalendarFrequencyType calendarFrequencyType = CalendarFrequencyType.INVALID; Integer updatedRepeatsOnDay = repeatsOnDay; - switch (recalculationFrequencyType) { - case DAILY: - calendarFrequencyType = CalendarFrequencyType.DAILY; - break; - case MONTHLY: - calendarFrequencyType = CalendarFrequencyType.MONTHLY; - break; - case SAME_AS_REPAYMENT_PERIOD: - frequency = loan.repaymentScheduleDetail().getRepayEvery(); - calendarFrequencyType = CalendarFrequencyType.from(loan.repaymentScheduleDetail().getRepaymentPeriodFrequencyType()); - calendarStartDate = loan.getExpectedDisbursedOnLocalDate(); - if (updatedRepeatsOnDay == null) { - updatedRepeatsOnDay = calendarStartDate.get(ChronoField.DAY_OF_WEEK); - } - break; - case WEEKLY: - calendarFrequencyType = CalendarFrequencyType.WEEKLY; - break; - default: - break; + final CalendarFrequencyType calendarFrequencyType = switch (recalculationFrequencyType) { + case DAILY -> CalendarFrequencyType.DAILY; + case WEEKLY -> CalendarFrequencyType.WEEKLY; + case MONTHLY -> CalendarFrequencyType.MONTHLY; + case SAME_AS_REPAYMENT_PERIOD -> CalendarFrequencyType.from(loan.repaymentScheduleDetail().getRepaymentPeriodFrequencyType()); + case INVALID -> CalendarFrequencyType.INVALID; + }; + + if (recalculationFrequencyType == SAME_AS_REPAYMENT_PERIOD) { + frequency = loan.repaymentScheduleDetail().getRepayEvery(); + calendarStartDate = loan.getExpectedDisbursedOnLocalDate(); + if (updatedRepeatsOnDay == null) { + updatedRepeatsOnDay = calendarStartDate.get(ChronoField.DAY_OF_WEEK); + } } final Calendar calendar = Calendar.createRepeatingCalendar(title, calendarStartDate, CalendarType.COLLECTION.getValue(), diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java index ef7152c94..346f1fcfb 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java @@ -971,11 +971,15 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo final String defaultUserMessage = "Installment charge cannot be added to the loan."; throw new LoanChargeCannotBeAddedException("loanCharge", "overdue.charge", defaultUserMessage, null, chargeDefinition.getName()); - } else if (loanCharge.getDueLocalDate() != null - && DateUtils.isBefore(loanCharge.getDueLocalDate(), loan.getLastUserTransactionForChargeCalc())) { - final String defaultUserMessage = "charge with date before last transaction date can not be added to loan."; - throw new LoanChargeCannotBeAddedException("loanCharge", "date.is.before.last.transaction.date", defaultUserMessage, null, - chargeDefinition.getName()); + } else if (loanCharge.getDueLocalDate() != null) { + // TODO: Review, error message seems not valid if interest recalculation is not enabled. + LocalDate validationDate = loan.repaymentScheduleDetail().isInterestRecalculationEnabled() ? loan.getLastUserTransactionDate() + : loan.getDisbursementDate(); + if (DateUtils.isBefore(loanCharge.getDueLocalDate(), validationDate)) { + final String defaultUserMessage = "charge with date before last transaction date can not be added to loan."; + throw new LoanChargeCannotBeAddedException("loanCharge", "date.is.before.last.transaction.date", defaultUserMessage, null, + chargeDefinition.getName()); + } } else if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { if (loanCharge.isInstalmentFee() && loan.getStatus().isActive()) { final String defaultUserMessage = "installment charge addition not allowed after disbursement";