From abe28e02e9e5e9b23bd2001b67e2b16eced6bd76 Mon Sep 17 00:00:00 2001 From: Kristof Jozsa Date: Mon, 10 Jun 2024 16:04:05 +0200 Subject: [PATCH] refactor and cleanup Loan entity --- .../portfolio/loanaccount/domain/Loan.java | 1472 ++++--------- .../loanaccount/domain/LoanSummary.java | 153 +- .../LoanApplicationValidator.java | 1935 +++++++++-------- ...WritePlatformServiceJpaRepositoryImpl.java | 37 +- 4 files changed, 1468 insertions(+), 2129 deletions(-) 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 1d07b282efb..d4cba7ebf22 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 @@ -60,6 +60,7 @@ import java.util.Optional; import java.util.Set; import java.util.function.Predicate; +import java.util.stream.Collectors; import lombok.Getter; import lombok.Setter; import org.apache.commons.lang3.StringUtils; @@ -140,7 +141,6 @@ 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; @@ -157,31 +157,10 @@ 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"; @@ -197,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; @@ -208,106 +187,123 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { private ExternalId externalId; @ManyToOne - @JoinColumn(name = "client_id", nullable = true) + @JoinColumn(name = "client_id") private Client client; + @Getter @ManyToOne - @JoinColumn(name = "group_id", nullable = true) + @JoinColumn(name = "group_id") private Group group; + @Getter + @Setter @ManyToOne - @JoinColumn(name = "glim_id", nullable = true) + @JoinColumn(name = "glim_id") private GroupLoanIndividualMonitoringAccount glim; + @Setter @Column(name = "loan_type_enum", nullable = false) private Integer loanType; + @Getter @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "product_id", nullable = false) 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; + @Getter @Column(name = "loan_transaction_strategy_code", nullable = false) private String transactionProcessingStrategyCode; + @Getter @Column(name = "loan_transaction_strategy_name") private String transactionProcessingStrategyName; // TODO FINERACT-1932-Fineract modularization: Move to fineract-progressive-loan module after removing association // from Loan entity + @Setter + @Getter @OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true, fetch = FetchType.LAZY) private List paymentAllocationRules = new ArrayList<>(); // TODO FINERACT-1932-Fineract modularization: Move to fineract-progressive-loan module after removing association // from Loan entity + @Setter + @Getter @OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true, fetch = FetchType.LAZY) private List creditAllocationRules = new ArrayList<>(); @Embedded private LoanProductRelatedDetail loanRepaymentScheduleDetail; + @Getter @Column(name = "term_frequency", nullable = false) private Integer termFrequency; + @Getter @Column(name = "term_period_frequency_enum", nullable = false) private Integer termPeriodFrequencyType; @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 + @Getter @Column(name = "submittedon_date") private LocalDate submittedOnDate; @Column(name = "rejectedon_date") 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; + @Getter @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") private LocalDate expectedDisbursementDate; + @Setter @Column(name = "disbursedon_date") private LocalDate actualDisbursementDate; @ManyToOne(optional = true, fetch = FetchType.LAZY) - @JoinColumn(name = "disbursedon_userid", nullable = true) + @JoinColumn(name = "disbursedon_userid") private AppUser disbursedBy; + @Getter @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") @@ -317,24 +313,30 @@ 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; + @Getter @Column(name = "expected_maturedon_date") private LocalDate expectedMaturityDate; @Column(name = "maturedon_date") private LocalDate actualMaturityDate; + @Getter + @Setter @Column(name = "expected_firstrepaymenton_date") private LocalDate expectedFirstRepaymentOnDate; + @Getter @Column(name = "interest_calculated_from_date") private LocalDate interestChargedFromDate; + @Getter @Column(name = "total_overpaid_derived", scale = 6, precision = 19) private BigDecimal totalOverpaid; + @Getter @Column(name = "overpaidon_date") private LocalDate overpaidOnDate; @@ -353,6 +355,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { @OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true, fetch = FetchType.LAZY) private Set collateral = null; + @Getter @OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true, fetch = FetchType.LAZY) private Set loanCollateralManagements = new HashSet<>(); @@ -363,6 +366,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { @OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true, fetch = FetchType.LAZY) private List repaymentScheduleInstallments = new ArrayList<>(); + @Getter @OrderBy(value = "dateOf, createdDate, id") @OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true, fetch = FetchType.LAZY) private List loanTransactions = new ArrayList<>(); @@ -370,8 +374,9 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { @Embedded private LoanSummary summary; + @Getter @Transient - private boolean accountNumberRequiresAutoGeneration = false; + private boolean accountNumberRequiresAutoGeneration; @Transient private LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory; @@ -386,19 +391,21 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { @Column(name = "approved_principal", scale = 6, precision = 19, nullable = false) private BigDecimal approvedPrincipal; + @Setter @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) @OrderBy(value = "expectedDisbursementDate, id") private List disbursementDetails = new ArrayList<>(); + @Getter @OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true, fetch = FetchType.LAZY) private List postDatedChecks = new ArrayList<>(); @@ -409,6 +416,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { @Column(name = "total_recovered_derived", scale = 6, precision = 19) private BigDecimal totalRecovered; + @Getter @OneToOne(cascade = CascadeType.ALL, mappedBy = "loan", optional = true, orphanRemoval = true, fetch = FetchType.LAZY) private LoanInterestRecalculationDetails loanInterestRecalculationDetails; @@ -418,47 +426,54 @@ 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) + @Getter + @Column(name = "loan_sub_status_id") private Integer loanSubStatus; @Column(name = "is_topup", nullable = false) private boolean isTopup = false; + @Getter @Column(name = "is_fraud", nullable = false) private boolean fraud = false; @OneToOne(cascade = CascadeType.ALL, mappedBy = "loan", optional = true, orphanRemoval = true, fetch = FetchType.LAZY) private LoanTopupDetails loanTopupDetails; + @Getter + @Setter @OneToMany(fetch = FetchType.LAZY) @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; + @Getter + @Setter @Column(name = "last_closed_business_date") private LocalDate lastClosedBusinessDate; + @Getter @Column(name = "is_charged_off", nullable = false) private boolean chargedOff; @@ -466,6 +481,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { @JoinColumn(name = "charge_off_reason_cv_id") private CodeValue chargeOffReason; + @Getter @Column(name = "charged_off_on_date") private LocalDate chargedOffOnDate; @@ -473,6 +489,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { @JoinColumn(name = "charged_off_by_userid") private AppUser chargedOffBy; + @Getter @Column(name = "enable_installment_level_delinquency", nullable = false) private boolean enableInstallmentLevelDelinquency = false; @@ -528,7 +545,7 @@ public static Loan newIndividualLoanApplicationFromGroup(final String accountNo, } protected Loan() { - this.client = null; + // empty } private Loan(final String accountNo, final Client client, final Group group, final AccountType loanType, final Fund fund, @@ -657,16 +674,10 @@ public void updateLoanSummaryForUndoWaiveCharge(final BigDecimal amountWaived, f } 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) { @@ -686,12 +697,7 @@ private Set associateWithThisLoan(final Set(); } - this.charges.add(loanCharge); - this.summary = updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement()); // store Id's of existing loan transactions and existing reversed loan @@ -748,7 +752,7 @@ public void addLoanCharge(final LoanCharge loanCharge) { } public ChangedTransactionDetail reprocessTransactions() { - ChangedTransactionDetail changedTransactionDetail = null; + ChangedTransactionDetail changedTransactionDetail; final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory .determineProcessor(this.transactionProcessingStrategyCode); final List allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing(); @@ -769,10 +773,6 @@ public ChangedTransactionDetail reprocessTransactions() { * * 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()); @@ -783,8 +783,7 @@ public LoanTransaction handleChargeAppliedTransaction(final LoanCharge loanCharg feeCharges = Money.zero(loanCurrency()); } - LocalDate transactionDate = null; - + LocalDate transactionDate; if (suppliedTransactionDate != null) { transactionDate = suppliedTransactionDate; } else { @@ -886,7 +885,6 @@ private LocalDate getLastRepaymentPeriodDueDate(final boolean includeRecalculate } public void removeLoanCharge(final LoanCharge loanCharge) { - validateLoanIsNotClosed(loanCharge); // NOTE: to remove this constraint requires that loan transactions @@ -907,13 +905,13 @@ public void removeLoanCharge(final LoanCharge loanCharge) { 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()); @@ -929,7 +927,6 @@ private void removeOrModifyTransactionAssociatedWithLoanChargeIfDueAtDisbursemen 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)) { @@ -952,19 +949,15 @@ && doesLoanChargePaidByContainLoanCharge(transaction.getLoanChargesPaid(), loanC } 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); @@ -975,13 +968,13 @@ public Map updateLoanCharge(final LoanCharge loanCharge, final J 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()); @@ -996,41 +989,31 @@ public Map updateLoanCharge(final LoanCharge loanCharge, final J 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() { @@ -1043,9 +1026,6 @@ private Money getTotalAllTrancheDisbursementAmount() { return amount; } - /** - * @return - */ public BigDecimal getTotalInterest() { return this.loanSummaryWrapper.calculateTotalInterestCharged(getRepaymentScheduleInstallments(), getCurrency()).getAmount(); } @@ -1075,34 +1055,25 @@ public BigDecimal getTotalWrittenOff() { */ 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(); @@ -1196,14 +1167,6 @@ public Client client() { return this.client; } - public GroupLoanIndividualMonitoringAccount getGlim() { - return glim; - } - - public void setGlim(GroupLoanIndividualMonitoringAccount glim) { - this.glim = glim; - } - public LoanProduct loanProduct() { return this.loanProduct; } @@ -1253,7 +1216,7 @@ public void updateTransactionProcessingStrategy(final String transactionProcessi 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 @@ -1295,7 +1258,7 @@ public void updateLoanCharges(final Set loanCharges) { } - /** Updated deleted charges **/ + /* Updated deleted charges **/ for (Long id : existingCharges) { fetchLoanChargesById(id).setActive(false); } @@ -1336,7 +1299,7 @@ public void updateLoanSchedule(final LoanScheduleModel modifiedLoanSchedule) { updateLoanScheduleDependentDerivedFields(); updateLoanSummaryDerivedFields(); - applyAccurals(); + applyAccruals(); } public void updateLoanSchedule(final Collection installments) { @@ -1355,7 +1318,7 @@ public void updateLoanSchedule(final Collection accruals = retrieveListOfAccrualTransactions(); if (!accruals.isEmpty()) { if (isPeriodicAccrualAccountingEnabledOnLoanProduct()) { @@ -1390,28 +1353,28 @@ private void applyPeriodicAccruals(final Collection accruals) { 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) { @@ -1452,11 +1415,9 @@ private void updateAccrualsForNonPeriodicAccruals(final Collection 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 @@ -1673,28 +1624,21 @@ private Map parseDisbursementDetails(final JsonObject jsonObject } 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)) { @@ -1709,7 +1653,7 @@ public void updateDisbursementDetails(final JsonCommand jsonCommand, final Map chargeId = Splitter.on(',').split(chargeIds); for (String loanChargeId : chargeId) { loanChargeIds.remove(Long.parseLong(loanChargeId)); @@ -1720,7 +1664,7 @@ public void updateDisbursementDetails(final JsonCommand jsonCommand, final Map ac private void createOrUpdateDisbursementDetails(Long disbursementID, final Map actualChanges, LocalDate expectedDisbursementDate, BigDecimal principal, List existingDisbursementList) { - if (disbursementID != null) { LoanDisbursementDetails loanDisbursementDetail = fetchLoanDisbursementsById(disbursementID); existingDisbursementList.remove(disbursementID); @@ -1793,45 +1736,32 @@ private void createOrUpdateDisbursementDetails(Long disbursementID, final Map 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() { @@ -1852,7 +1782,6 @@ private LocalDate determineExpectedMaturityDate() { public Map loanApplicationRejection(final AppUser currentUser, final JsonCommand command, final LoanLifecycleStateMachine loanLifecycleStateMachine) { - validateAccountStatus(LoanEvent.LOAN_REJECTED); final Map actualChanges = new LinkedHashMap<>(); @@ -1900,7 +1829,6 @@ public Map loanApplicationRejection(final AppUser currentUser, f 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); @@ -1949,7 +1877,6 @@ public Map loanApplicationWithdrawnByApplicant(final AppUser cur public Map loanApplicationApproval(final AppUser currentUser, final JsonCommand command, final JsonArray disbursementDataArray, final LoanLifecycleStateMachine loanLifecycleStateMachine) { - validateAccountStatus(LoanEvent.LOAN_APPROVED); final Map actualChanges = new LinkedHashMap<>(); @@ -1979,7 +1906,7 @@ public Map loanApplicationApproval(final AppUser currentUser, fi 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) { @@ -1996,7 +1923,6 @@ public Map loanApplicationApproval(final AppUser currentUser, fi actualChanges.put(LoanApiConstants.disbursementPrincipalParameterName, approvedLoanAmount); actualChanges.put(LoanApiConstants.disbursementNetDisbursalAmountParameterName, netDisbursalAmount); - /* Update disbursement details */ if (disbursementDataArray != null) { updateDisbursementDetails(command, actualChanges); } @@ -2005,21 +1931,10 @@ public Map loanApplicationApproval(final AppUser currentUser, fi 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()); } @@ -2037,15 +1952,15 @@ public Map loanApplicationApproval(final AppUser currentUser, fi 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); } } @@ -2065,11 +1980,25 @@ public Map loanApplicationApproval(final AppUser currentUser, fi } 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) { @@ -2088,20 +2017,16 @@ private void compareApprovedToProposedPrincipal(BigDecimal approvedLoanAmount) { } 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<>(); @@ -2132,31 +2057,20 @@ public Map undoApproval(final LoanLifecycleStateMachine loanLife } 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; @@ -2181,11 +2095,10 @@ public ChangedTransactionDetail disburse(final AppUser currentUser, final JsonCo 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(); @@ -2201,12 +2114,10 @@ && isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct()) { 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); @@ -2214,15 +2125,9 @@ private void regenerateRepaymentScheduleWithInterestRecalculationIfNeeded(boolea } 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, @@ -2341,7 +2246,6 @@ public Money adjustDisburseAmount(@NotNull JsonCommand command, @NotNull LocalDa } private void compareDisbursedToApprovedOrProposedPrincipal(BigDecimal disbursedAmount, BigDecimal totalDisbursed) { - if (this.loanProduct().isDisallowExpectedDisbursements() && this.loanProduct().isAllowApprovedDisbursedAmountsOverApplied()) { BigDecimal maxDisbursedAmount = getOverAppliedMax(); if (totalDisbursed.compareTo(maxDisbursedAmount) > 0) { @@ -2378,7 +2282,6 @@ private ChangedTransactionDetail reprocessTransactionForDisbursement() { } updateLoanSummaryDerivedFields(); } - return changedTransactionDetail; } @@ -2417,15 +2320,9 @@ private LoanDisbursementDetails fetchLastDisburseDetail() { } 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() { @@ -2439,39 +2336,17 @@ public BigDecimal getDisbursedAmount() { } 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) { @@ -2481,7 +2356,7 @@ private void updateLoanRepaymentPeriodsDerivedFields(final LocalDate actualDisbu } } - /* + /** * Ability to regenerate the repayment schedule based on the loans current details/state. */ public void regenerateRepaymentSchedule(final ScheduleGeneratorDTO scheduleGeneratorDTO) { @@ -2499,7 +2374,6 @@ public void regenerateRepaymentSchedule(final ScheduleGeneratorDTO scheduleGener } public LoanScheduleModel regenerateScheduleModel(final ScheduleGeneratorDTO scheduleGeneratorDTO) { - final MathContext mc = MoneyHelper.getMathContext(); final LoanApplicationTerms loanApplicationTerms = constructLoanApplicationTerms(scheduleGeneratorDTO); @@ -2550,19 +2424,18 @@ private BigDecimal constructFloatingInterestRates(final BigDecimal annualNominal } 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, @@ -2570,9 +2443,9 @@ private void handleDisbursementTransaction(final LocalDate disbursedOn, final Pa 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()) @@ -2623,7 +2496,6 @@ private void handleDisbursementTransaction(final LocalDate disbursedOn, final Pa + " 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, @@ -2657,6 +2529,7 @@ public LoanTransaction handleDownPayment(final LoanTransaction disbursementTrans case PROGRESSIVE -> MathUtil.negativeToZero(downPaymentMoney.minus(disbursementTransaction.getOverPaymentPortion(getCurrency()))); }; + if (adjustedDownPaymentMoney.isGreaterThanZero()) { LoanTransaction downPaymentTransaction = LoanTransaction.downPayment(getOffice(), adjustedDownPaymentMoney, null, disbursedOn, externalId); @@ -2680,7 +2553,7 @@ public boolean isAutoRepaymentForDownPaymentEnabled() { && 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()); @@ -2690,7 +2563,6 @@ public LoanTransaction handlePayDisbursementTransaction(final Long chargeId, fin charge = loanCharge; } } - @SuppressWarnings("null") final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(chargesPayment, charge, charge.amount(), null); chargesPayment.getLoanChargesPaid().add(loanChargePaidBy); final Money zero = Money.zero(getCurrency()); @@ -2699,17 +2571,14 @@ public LoanTransaction handlePayDisbursementTransaction(final Long chargeId, fin 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<>(); @@ -2763,11 +2632,8 @@ public Map undoDisbursal(final ScheduleGeneratorDTO scheduleGene } this.adjustNetDisbursalAmount(this.approvedPrincipal); - actualChanges.put(ACTUAL_DISBURSEMENT_DATE, ""); - updateLoanSummaryDerivedFields(); - } return actualChanges; @@ -2815,9 +2681,7 @@ private void updateLoanToPreDisbursalState() { 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()); @@ -2834,13 +2698,9 @@ public ChangedTransactionDetail makeRepayment(final LoanTransaction repaymentTra 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(); } @@ -2864,7 +2724,6 @@ private void validateRepaymentTypeAccountStatus(LoanTransaction repaymentTransac 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"; @@ -2882,7 +2741,6 @@ private void validateRepaymentTypeAccountStatus(LoanTransaction repaymentTransac 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()); @@ -2911,7 +2769,6 @@ public void makeRefund(final LoanTransaction loanTransaction, final LoanLifecycl 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); @@ -2944,7 +2801,6 @@ public void makeRefund(final LoanTransaction loanTransaction, final LoanLifecycl private ChangedTransactionDetail handleRepaymentOrRecoveryOrWaiverTransaction(final LoanTransaction loanTransaction, final LoanLifecycleStateMachine loanLifecycleStateMachine, final LoanTransaction adjustedTransaction, final ScheduleGeneratorDTO scheduleGeneratorDTO) { - ChangedTransactionDetail changedTransactionDetail = null; if (loanTransaction.isRecoveryRepayment()) { @@ -2971,13 +2827,7 @@ private ChangedTransactionDetail handleRepaymentOrRecoveryOrWaiverTransaction(fi 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."; @@ -3011,13 +2861,10 @@ private ChangedTransactionDetail handleRepaymentOrRecoveryOrWaiverTransaction(fi 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()) { @@ -3046,7 +2893,7 @@ private ChangedTransactionDetail handleRepaymentOrRecoveryOrWaiverTransaction(fi 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. */ @@ -3076,86 +2923,59 @@ private ChangedTransactionDetail handleRepaymentOrRecoveryOrWaiverTransaction(fi 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() { - final List repaymentsOrWaivers = new ArrayList<>(); - for (final LoanTransaction transaction : this.loanTransactions) { - if (transaction.isNotReversed() && !(transaction.isAccrual() || transaction.isNonMonetaryTransaction())) { - repaymentsOrWaivers.add(transaction); - } - } - repaymentsOrWaivers.sort(LoanTransactionComparator.INSTANCE); - return repaymentsOrWaivers; + return this.loanTransactions.stream() + .filter(transaction -> transaction.isNotReversed() && !transaction.isAccrual() && !transaction.isNonMonetaryTransaction()) + .sorted(LoanTransactionComparator.INSTANCE).collect(Collectors.toList()); } 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, @@ -3179,15 +2999,10 @@ private boolean doPostLoanTransactionChecks(final LocalDate transactionDate, } 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; @@ -3286,60 +3101,33 @@ private void handleLoanOverpayment(LocalDate transactionDate, final LoanLifecycl private boolean isChronologicallyLatestRepaymentOrWaiver(final LoanTransaction loanTransaction, final List loanTransactions) { - boolean isChronologicallyLatestRepaymentOrWaiver = true; - - final LocalDate currentTransactionDate = loanTransaction.getTransactionDate(); - for (final LoanTransaction previousTransaction : loanTransactions) { - if (!previousTransaction.isDisbursement() && previousTransaction.isNotReversed() - && (DateUtils.isBefore(currentTransactionDate, previousTransaction.getTransactionDate()) - || (DateUtils.isEqual(currentTransactionDate, previousTransaction.getTransactionDate()) - && ((loanTransaction.getId() == null && previousTransaction.getId() == null) - || (loanTransaction.getId() != null && (previousTransaction.getId() == null - || loanTransaction.getId().compareTo(previousTransaction.getId()) < 0)))))) { - isChronologicallyLatestRepaymentOrWaiver = false; - break; - } - } - return isChronologicallyLatestRepaymentOrWaiver; + return loanTransactions.stream() + .noneMatch(previousTransaction -> !previousTransaction.isDisbursement() && !previousTransaction.isReversed() + && (DateUtils.isAfter(loanTransaction.getTransactionDate(), previousTransaction.getTransactionDate()) + || (DateUtils.isEqual(loanTransaction.getTransactionDate(), previousTransaction.getTransactionDate()) + && (loanTransaction.getId() == null || previousTransaction.getId() == null + || loanTransaction.getId().compareTo(previousTransaction.getId()) > 0)))); } 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; + return loanTransactions.stream() // + .filter(t -> t.isRepaymentLikeType() && t.isNotReversed()) // + .noneMatch(t -> DateUtils.isAfter(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) // + .noneMatch(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(); - } - return nextPossibleRepaymentDate; + return switch (nextPaymentDueDateConfig.toLowerCase()) { + case EARLIEST_UNPAID_DATE -> getEarliestUnpaidInstallmentDate(); + case NEXT_UNPAID_DUE_DATE -> getNextUnpaidInstallmentDueDate(); + default -> null; + }; } private LocalDate getNextUnpaidInstallmentDueDate() { @@ -3390,21 +3178,7 @@ private LocalDate getEarliestUnpaidInstallmentDate() { 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(); @@ -3433,7 +3207,6 @@ public ChangedTransactionDetail adjustExistingTransaction(final LoanTransaction 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(), @@ -3465,7 +3238,7 @@ public ChangedTransactionDetail adjustExistingTransaction(final LoanTransaction writeOffTransaction.reverse(); } - if (isClosedObligationsMet() || isClosedWrittenOff() || isClosedWithOutsandingAmountMarkedForReschedule()) { + if (isClosedObligationsMet() || isClosedWrittenOff() || isClosedWithOutstandingAmountMarkedForReschedule()) { loanLifecycleStateMachine.transition(LoanEvent.LOAN_ADJUST_TRANSACTION, this); } @@ -3480,7 +3253,6 @@ public ChangedTransactionDetail adjustExistingTransaction(final LoanTransaction 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()); @@ -3501,15 +3273,9 @@ public ChangedTransactionDetail undoWrittenOff(LoanLifecycleStateMachine loanLif } 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() { @@ -3517,7 +3283,6 @@ private boolean isOverPaid() { } private Money calculateTotalOverpayment() { - Money totalPaidInRepayments = getTotalPaidInRepayments(); final MonetaryCurrency currency = loanCurrency(); @@ -3525,7 +3290,6 @@ private Money calculateTotalOverpayment() { 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)); @@ -3544,21 +3308,21 @@ private Money calculateTotalOverpayment() { 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(); } @@ -3641,7 +3405,7 @@ public ChangedTransactionDetail closeAsWrittenOff(final JsonCommand command, fin 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); @@ -3663,14 +3427,9 @@ private ChangedTransactionDetail closeDisbursements(final ScheduleGeneratorDTO s } 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; + return this.loanTransactions.stream() // + .filter(loanTransaction -> !loanTransaction.isReversed()) // + .max(Comparator.comparing(LoanTransaction::getTransactionDate)).orElse(null); } public ChangedTransactionDetail close(final JsonCommand command, final LoanLifecycleStateMachine loanLifecycleStateMachine, @@ -3772,7 +3531,6 @@ public ChangedTransactionDetail close(final JsonCommand command, final LoanLifec */ public void closeAsMarkedForReschedule(final JsonCommand command, final LoanLifecycleStateMachine loanLifecycleStateMachine, final Map changes) { - final LocalDate rescheduledOn = command.localDateValueOfParameterNamed(TRANSACTION_DATE); this.closedOnDate = rescheduledOn; @@ -3825,13 +3583,7 @@ private boolean isNotDisbursed() { } 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() { @@ -3850,7 +3602,7 @@ public boolean isClosedWrittenOff() { return getStatus().isClosedWrittenOff(); } - private boolean isClosedWithOutsandingAmountMarkedForReschedule() { + private boolean isClosedWithOutstandingAmountMarkedForReschedule() { return getStatus().isClosedWithOutsandingAmountMarkedForReschedule(); } @@ -3877,28 +3629,15 @@ private boolean isAllTranchesNotDisbursed() { } 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; } @@ -3911,14 +3650,6 @@ public LocalDate getDisbursementDate() { 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) { @@ -3946,10 +3677,6 @@ public BigDecimal getDisburseAmountForTemplate() { return principal; } - public LocalDate getExpectedFirstRepaymentOnDate() { - return this.expectedFirstRepaymentOnDate; - } - private boolean isActualDisbursedOnDateEarlierOrLaterThanExpected(final LocalDate actualDisbursedOnDate) { boolean isRegenerationRequired = false; if (this.loanProduct.isMultiDisburseLoan()) { @@ -4030,19 +3757,11 @@ public boolean hasIdentifyOf(final Long loanId) { } 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() { @@ -4089,8 +3808,7 @@ public void reassignLoanOfficer(final Staff newLoanOfficer, final LocalDate assi 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); } @@ -4104,7 +3822,6 @@ public void reassignLoanOfficer(final Staff newLoanOfficer, final LocalDate assi } public void removeLoanOfficer(final LocalDate unassignDate) { - final LoanOfficerAssignmentHistory latestHistoryRecord = findLatestIncompleteHistoryRecord(); if (latestHistoryRecord != null) { @@ -4157,47 +3874,23 @@ private LoanOfficerAssignmentHistory findLastAssignmentHistoryRecord(final Staff } 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() { @@ -4225,18 +3918,6 @@ public Long productId() { 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) { @@ -4310,35 +3991,41 @@ private Map buildAccountingMapForChargeOffDateCriteria(final Str 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()); - - if (transaction.isReversed() && isExistingTransaction && !isExistingReversedTransaction) { - return true; - } else { - return !isExistingTransaction; - } - }).map(transaction -> transaction.toMapData(currencyCode)).toList()); + 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()); } private void filterTransactionsByChargeOffDate(List> newLoanTransactionsBeforeChargeOff, List> newLoanTransactionsAfterChargeOff, String currencyCode, List existingTransactionIds, List existingReversedTransactionIds, Predicate chargeOffDateCriteria) { - LoanTransaction chargeOffTransaction = this.loanTransactions.stream().filter(LoanTransaction::isChargeOff) + LoanTransaction chargeOffTransaction = this.loanTransactions.stream() // + .filter(LoanTransaction::isChargeOff) // .filter(LoanTransaction::isNotReversed).findFirst().get(); - this.loanTransactions.stream().filter(chargeOffDateCriteria).forEach(transaction -> { - boolean isExistingTransaction = existingTransactionIds.contains(transaction.getId()); - boolean isExistingReversedTransaction = existingReversedTransactionIds.contains(transaction.getId()); - List> targetList = (transaction.getId().compareTo(chargeOffTransaction.getId()) < 0) - ? newLoanTransactionsBeforeChargeOff - : newLoanTransactionsAfterChargeOff; - if ((transaction.isReversed() && isExistingTransaction && !isExistingReversedTransaction) || !isExistingTransaction) { - targetList.add(transaction.toMapData(currencyCode)); - } - }); + this.loanTransactions.stream() // + .filter(chargeOffDateCriteria) // + .forEach(transaction -> { + boolean isExistingTransaction = existingTransactionIds.contains(transaction.getId()); + boolean isExistingReversedTransaction = existingReversedTransactionIds.contains(transaction.getId()); + List> targetList = (transaction.getId().compareTo(chargeOffTransaction.getId()) < 0) + ? newLoanTransactionsBeforeChargeOff + : newLoanTransactionsAfterChargeOff; + if ((transaction.isReversed() && isExistingTransaction && !isExistingReversedTransaction) || !isExistingTransaction) { + targetList.add(transaction.toMapData(currencyCode)); + } + }); } public Map deriveAccountingBridgeData(final String currencyCode, final List existingTransactionIds, @@ -4406,10 +4093,6 @@ public boolean isSyncDisbursementWithMeeting() { 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) { @@ -4421,8 +4104,8 @@ public void updateLoanRepaymentScheduleDates(final String recurringRule, final b 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) { @@ -4438,6 +4121,7 @@ public void updateLoanRepaymentScheduleDates(final String recurringRule, final b newRepaymentDate = CalendarUtils.getNewRepaymentMeetingDate(recurringRule, tmpFromDate, tmpFromDate.plusDays(1), loanRepaymentInterval, frequency, workingDays, isSkipRepaymentOnFirstDayOfMonth, numberOfDays); } + if (isHolidayEnabled) { newRepaymentDate = HolidayUtil.getRepaymentRescheduleDateToIfHoliday(newRepaymentDate, holidays); } @@ -4447,12 +4131,9 @@ public void updateLoanRepaymentScheduleDates(final String recurringRule, final b 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; } @@ -4502,8 +4183,7 @@ public void updateLoanRepaymentScheduleDates(final LocalDate meetingStartDate, f 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; } @@ -4516,27 +4196,16 @@ public void updateLoanRepaymentScheduleDates(final LocalDate meetingStartDate, f 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; - } - return dueRepaymentPeriodDate.minusDays(1);// get 2n-1 range date from - // startDate + 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 } private void validateDisbursementDateIsOnNonWorkingDay(final WorkingDays workingDays, final boolean allowTransactionsOnNonWorkingDay) { @@ -4608,19 +4277,6 @@ public void updateInterestRateFrequencyType() { 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); } @@ -4643,68 +4299,68 @@ private void validateActivityNotBeforeClientOrGroupTransferDate(final LoanEvent 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); } @@ -4721,23 +4377,23 @@ private void validateActivityNotBeforeLastTransactionDate(final LoanEvent event, 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); } @@ -4796,33 +4452,11 @@ public LoanTransaction getLastRepaymentOrDownPaymentTransaction() { } public LocalDate getLastUserTransactionForChargeCalc() { - LocalDate lastTransaction = getDisbursementDate(); - if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) { - lastTransaction = getLastUserTransactionDate(); - } - return lastTransaction; + return this.repaymentScheduleDetail().isInterestRecalculationEnabled() ? getLastUserTransactionDate() : getLastRepaymentDate(); } 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) { @@ -4849,27 +4483,26 @@ public List generateInstallmentLoanCharges(final LoanChar } 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."; @@ -4877,8 +4510,8 @@ public void validateAccountStatus(final LoanEvent event) { .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", @@ -4891,121 +4524,121 @@ public void validateAccountStatus(final LoanEvent event) { .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()) { @@ -5146,35 +4779,10 @@ public LoanRepaymentScheduleInstallment fetchRepaymentScheduleInstallment(final 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()); } @@ -5194,10 +4802,6 @@ public boolean isInterestBearing() { return BigDecimal.ZERO.compareTo(getLoanRepaymentScheduleDetail().getAnnualNominalInterestRate()) < 0; } - public LocalDate getExpectedMaturityDate() { - return this.expectedMaturityDate; - } - public LocalDate getMaturityDate() { return this.actualMaturityDate; } @@ -5292,7 +4896,7 @@ public void regenerateRepaymentScheduleWithInterestRecalculation(final ScheduleG 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"); @@ -5408,7 +5012,7 @@ private void addUpdateIncomeAndAccrualTransaction(LoanInterestRecalcualtionAddit 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; @@ -5519,10 +5123,9 @@ private LoanScheduleDTO getRecalculatedSchedule(final ScheduleGeneratorDTO gener } 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(); @@ -5629,18 +5232,14 @@ private LoanRepaymentScheduleInstallment getTotalOutstandingOnLoan() { 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() { @@ -5656,8 +5255,9 @@ private void updateLoanOutstandingBalances() { 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 @@ -5691,13 +5291,6 @@ public boolean isNpa() { return this.isNpa; } - /** - * @return List of loan repayments schedule objects - **/ - public List getRepaymentScheduleInstallments() { - return this.repaymentScheduleInstallments; - } - public Integer getLoanRepaymentScheduleInstallmentsSize() { return this.repaymentScheduleInstallments.size(); } @@ -5707,27 +5300,6 @@ public void addLoanRepaymentScheduleInstallment(final LoanRepaymentScheduleInsta 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 @@ -5777,18 +5349,6 @@ public List getDisbursmentData() { } /** - * @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" }) @@ -5797,8 +5357,6 @@ public LoanApplicationTerms getLoanApplicationTerms(final ApplicationCurrency ap 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(); @@ -5908,10 +5466,6 @@ public LoanProductRelatedDetail getLoanProductRelatedDetail() { return this.loanRepaymentScheduleDetail; } - public void updateNumberOfRepayments(Integer numberOfRepayments) { - this.loanRepaymentScheduleDetail.setNumberOfRepayments(numberOfRepayments); - } - public void updateRescheduledOnDate(LocalDate rescheduledOnDate) { if (rescheduledOnDate != null) { @@ -5919,13 +5473,6 @@ public void updateRescheduledOnDate(LocalDate rescheduledOnDate) { } } - public void updateTermFrequency(Integer termFrequency) { - - if (termFrequency != null) { - this.termFrequency = termFrequency; - } - } - public boolean isFeeCompoundingEnabledForInterestRecalculation() { boolean isEnabled = false; if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) { @@ -5934,18 +5481,6 @@ public boolean isFeeCompoundingEnabledForInterestRecalculation() { 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; } @@ -6015,7 +5550,6 @@ public ChangedTransactionDetail makeRefundForActiveLoan(final LoanTransaction lo 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()); @@ -6064,13 +5598,7 @@ private ChangedTransactionDetail handleRefundTransaction(final LoanTransaction l 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."; @@ -6090,7 +5618,7 @@ private ChangedTransactionDetail handleRefundTransaction(final LoanTransaction l 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()))); @@ -6113,7 +5641,6 @@ private ChangedTransactionDetail handleRefundTransaction(final LoanTransaction l 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); @@ -6133,7 +5660,6 @@ public void handleChargebackTransaction(final LoanTransaction chargebackTransact } public LocalDate possibleNextRefundDate() { - final LocalDate now = DateUtils.getBusinessLocalDate(); LocalDate lastTransactionDate = null; @@ -6198,16 +5724,11 @@ public Map undoLastDisbursal(ScheduleGeneratorDTO scheduleGenera 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"); @@ -6221,9 +5742,6 @@ public Map undoLastDisbursal(ScheduleGeneratorDTO scheduleGenera /** * Reverse only disbursement, accruals, and repayments at disbursal transactions - * - * @param lastDisbursalTransaction - * @return */ public void reverseExistingTransactionsTillLastDisbursal(LoanTransaction lastDisbursalTransaction) { for (final LoanTransaction transaction : this.loanTransactions) { @@ -6266,26 +5784,6 @@ private void updateLoanToLastDisbursalState(LoanDisbursementDetails disbursement 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) { @@ -6312,10 +5810,6 @@ public int fetchNumberOfInstallmensAfterExceptions() { 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 */ @@ -6343,10 +5837,8 @@ public BigDecimal getDerivedAmountForCharge(final LoanCharge loanCharge) { 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())) { @@ -6360,26 +5852,10 @@ public BigDecimal getDerivedAmountForCharge(final LoanCharge loanCharge) { 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()); @@ -6573,7 +6049,7 @@ public ChangedTransactionDetail handleForeClosureTransactions(final LoanTransact validateAccountStatus(event); validateForForeclosure(repaymentTransaction.getTransactionDate()); this.loanSubStatus = LoanSubStatus.FORECLOSED.getValue(); - applyAccurals(); + applyAccruals(); return handleRepaymentOrRecoveryOrWaiverTransaction(repaymentTransaction, loanLifecycleStateMachine, null, scheduleGeneratorDTO); } @@ -6673,10 +6149,6 @@ public void updateLoanScheduleOnForeclosure(final Collection getActiveLoanTermVariations() { return !retData.isEmpty() ? retData : null; } - public void setIsTopup(final boolean isTopup) { - this.isTopup = isTopup; - } - public boolean isTopup() { return this.isTopup; } @@ -6710,10 +6178,6 @@ public void markAsFraud(final boolean value) { this.fraud = value; } - public boolean isFraud() { - return this.fraud; - } - public BigDecimal getFirstDisbursalAmount() { BigDecimal firstDisbursalAmount; @@ -6777,26 +6241,10 @@ public boolean isIndividualLoan() { 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()); } @@ -6817,27 +6265,6 @@ public boolean hasDelinquencyBucket() { 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; @@ -6852,10 +6279,6 @@ public void liftChargeOff() { this.chargeOffReason = null; } - public boolean isChargedOff() { - return this.chargedOff; - } - public LoanRepaymentScheduleInstallment getLastLoanRepaymentScheduleInstallment() { return getRepaymentScheduleInstallments().get(getRepaymentScheduleInstallments().size() - 1); } @@ -6886,50 +6309,15 @@ public LoanTransaction getLastUserTransaction() { .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 void updateEnableInstallmentLevelDelinquency(boolean enableInstallmentLevelDelinquency) { + this.enableInstallmentLevelDelinquency = enableInstallmentLevelDelinquency; } - public boolean isEnableInstallmentLevelDelinquency() { - return this.enableInstallmentLevelDelinquency; + public void deductFromNetDisbursalAmount(final BigDecimal subtrahend) { + this.netDisbursalAmount = this.netDisbursalAmount.subtract(subtrahend); } - public void updateEnableInstallmentLevelDelinquency(boolean enableInstallmentLevelDelinquency) { - this.enableInstallmentLevelDelinquency = enableInstallmentLevelDelinquency; + 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 271f88f513c..1956682eb5d 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 @@ private LoanSummary(final BigDecimal totalFeeChargesDueAtDisbursement) { 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 void updateSummary(final MonetaryCurrency currency, final Money principal this.totalOutstanding = totalOutstanding.getAmount(); } - public BigDecimal getTotalPrincipalDisbursed() { - return this.totalPrincipalDisbursed; - } - - public BigDecimal getTotalPrincipalRepaid() { - return this.totalPrincipalRepaid; - } - - public BigDecimal getTotalWrittenOff() { - return this.totalWrittenOff; - } - - /** - * @return total interest repaid - **/ - public BigDecimal getTotalInterestRepaid() { - return this.totalInterestRepaid; + public void updateTotalFeeChargesDueAtDisbursement(final BigDecimal totalFeeChargesDueAtDisbursement) { + this.totalFeeChargesDueAtDisbursement = totalFeeChargesDueAtDisbursement; } - public BigDecimal getTotalFeeChargesCharged() { - return this.totalFeeChargesCharged; + public Money getTotalFeeChargesDueAtDisbursement(final MonetaryCurrency currency) { + return Money.of(currency, this.totalFeeChargesDueAtDisbursement); } - public BigDecimal getTotalPenaltyChargesCharged() { - return this.totalPenaltyChargesCharged; + public Money getTotalOutstanding(final MonetaryCurrency currency) { + return Money.of(currency, this.totalOutstanding); } - public BigDecimal getTotalPrincipalWrittenOff() { - return this.totalPrincipalWrittenOff; + public void updateFeeChargeOutstanding(final BigDecimal totalFeeChargesOutstanding) { + this.totalFeeChargesOutstanding = totalFeeChargesOutstanding; } - public BigDecimal getTotalInterestWaived() { - return this.totalInterestWaived; + public void updatePenaltyChargeOutstanding(final BigDecimal totalPenaltyChargesOutstanding) { + this.totalPenaltyChargesOutstanding = totalPenaltyChargesOutstanding; } - public BigDecimal getTotalFeeChargesRepaid() { - return this.totalFeeChargesRepaid; + public void updateFeeChargesWaived(final BigDecimal totalFeeChargesWaived) { + this.totalFeeChargesWaived = totalFeeChargesWaived; } - public BigDecimal getTotalFeeChargesWaived() { - return this.totalFeeChargesWaived; + public void updatePenaltyChargesWaived(final BigDecimal totalPenaltyChargesWaived) { + this.totalPenaltyChargesWaived = totalPenaltyChargesWaived; } - public BigDecimal getTotalPenaltyChargesRepaid() { - return this.totalPenaltyChargesRepaid; + public boolean isRepaidInFull(final MonetaryCurrency currency) { + return getTotalOutstanding(currency).isZero(); } - public BigDecimal getTotalPenaltyChargesWaived() { - return this.totalPenaltyChargesWaived; + public void updateTotalOutstanding(final BigDecimal newTotalOutstanding) { + this.totalOutstanding = newTotalOutstanding; } - 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 6cb8c922265..d94509a2c27 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.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 @@ private void validateForCreate(final JsonElement element) { 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 (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) { + // 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() - .integerGreaterThanZero(); + 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); + // if it is JLG loan then must have a value for + // syncDisbursement passed in + final Boolean syncDisbursement = this.fromApiJsonHelper + .extractBooleanNamed(LoanApiConstants.syncDisbursementWithMeetingParameterName, element); - if (syncDisbursement == null) { - baseDataValidator.reset().parameter(LoanApiConstants.syncDisbursementWithMeetingParameterName) - .value(syncDisbursement).trueOrFalseRequired(false); + 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)); - 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"); + 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); } - } - 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)) { + final String externalId = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.externalIdParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.externalIdParameterName).value(externalId).ignoreIfNull() + .notExceedingLengthOf(100); + } - baseDataValidator.reset().parameter(LoanApiConstants.productIdParameterName).value(productId).notNull().integerGreaterThanZero(); + 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.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.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.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.loanPurposeIdParameterName, element)) { + final Long loanPurposeId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.loanPurposeIdParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.loanPurposeIdParameterName).value(loanPurposeId).ignoreIfNull() + .integerGreaterThanZero(); + } - 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(); - } + final BigDecimal principal = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.principalParamName, + element); + baseDataValidator.reset().parameter(LoanApiConstants.principalParamName).value(principal).notNull().positiveAmount(); - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.loanOfficerIdParameterName, element)) { - final Long loanOfficerId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.loanOfficerIdParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.loanOfficerIdParameterName).value(loanOfficerId).ignoreIfNull() + final Integer loanTermFrequency = this.fromApiJsonHelper + .extractIntegerWithLocaleNamed(LoanApiConstants.loanTermFrequencyParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.loanTermFrequencyParameterName).value(loanTermFrequency).notNull() .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 Integer loanTermFrequencyType = this.fromApiJsonHelper + .extractIntegerSansLocaleNamed(LoanApiConstants.loanTermFrequencyTypeParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.loanTermFrequencyTypeParameterName).value(loanTermFrequencyType).notNull() + .inMinMaxRange(0, 3); - final BigDecimal principal = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.principalParamName, element); - baseDataValidator.reset().parameter(LoanApiConstants.principalParamName).value(principal).notNull().positiveAmount(); + final Integer numberOfRepayments = this.fromApiJsonHelper + .extractIntegerWithLocaleNamed(LoanApiConstants.numberOfRepaymentsParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.numberOfRepaymentsParameterName).value(numberOfRepayments).notNull() + .integerGreaterThanZero(); - final Integer loanTermFrequency = this.fromApiJsonHelper - .extractIntegerWithLocaleNamed(LoanApiConstants.loanTermFrequencyParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.loanTermFrequencyParameterName).value(loanTermFrequency).notNull() - .integerGreaterThanZero(); + final Integer repaymentEvery = this.fromApiJsonHelper + .extractIntegerWithLocaleNamed(LoanApiConstants.repaymentEveryParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.repaymentEveryParameterName).value(repaymentEvery).notNull() + .integerGreaterThanZero(); - final Integer loanTermFrequencyType = this.fromApiJsonHelper - .extractIntegerSansLocaleNamed(LoanApiConstants.loanTermFrequencyTypeParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.loanTermFrequencyTypeParameterName).value(loanTermFrequencyType).notNull() - .inMinMaxRange(0, 3); + final Integer repaymentEveryType = this.fromApiJsonHelper + .extractIntegerSansLocaleNamed(LoanApiConstants.repaymentFrequencyTypeParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.repaymentFrequencyTypeParameterName).value(repaymentEveryType).notNull() + .inMinMaxRange(0, 3); - final Integer numberOfRepayments = this.fromApiJsonHelper - .extractIntegerWithLocaleNamed(LoanApiConstants.numberOfRepaymentsParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.numberOfRepaymentsParameterName).value(numberOfRepayments).notNull() - .integerGreaterThanZero(); + CalendarUtils.validateNthDayOfMonthFrequency(baseDataValidator, LoanApiConstants.repaymentFrequencyNthDayTypeParameterName, + LoanApiConstants.repaymentFrequencyDayOfWeekTypeParameterName, element, this.fromApiJsonHelper); - final Integer repaymentEvery = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(LoanApiConstants.repaymentEveryParameterName, - element); - baseDataValidator.reset().parameter(LoanApiConstants.repaymentEveryParameterName).value(repaymentEvery).notNull() - .integerGreaterThanZero(); + final Integer interestType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(LoanApiConstants.interestTypeParameterName, + element); + baseDataValidator.reset().parameter(LoanApiConstants.interestTypeParameterName).value(interestType).notNull().inMinMaxRange(0, + 1); - final Integer repaymentEveryType = this.fromApiJsonHelper - .extractIntegerSansLocaleNamed(LoanApiConstants.repaymentFrequencyTypeParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.repaymentFrequencyTypeParameterName).value(repaymentEveryType).notNull() - .inMinMaxRange(0, 3); + final Integer interestCalculationPeriodType = this.fromApiJsonHelper + .extractIntegerSansLocaleNamed(LoanApiConstants.interestCalculationPeriodTypeParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.interestCalculationPeriodTypeParameterName) + .value(interestCalculationPeriodType).notNull().inMinMaxRange(0, 1); - CalendarUtils.validateNthDayOfMonthFrequency(baseDataValidator, LoanApiConstants.repaymentFrequencyNthDayTypeParameterName, - LoanApiConstants.repaymentFrequencyDayOfWeekTypeParameterName, element, this.fromApiJsonHelper); + 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."); + } - final Integer interestType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(LoanApiConstants.interestTypeParameterName, - element); - baseDataValidator.reset().parameter(LoanApiConstants.interestTypeParameterName).value(interestType).notNull().inMinMaxRange(0, 1); + 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); + } - final Integer interestCalculationPeriodType = this.fromApiJsonHelper - .extractIntegerSansLocaleNamed(LoanApiConstants.interestCalculationPeriodTypeParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.interestCalculationPeriodTypeParameterName) - .value(interestCalculationPeriodType).notNull().inMinMaxRange(0, 1); + 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."); + } - 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."); - } + 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)) { - final Boolean isFloatingInterestRate = this.fromApiJsonHelper.extractBooleanNamed(LoanApiConstants.isFloatingInterestRate, - element); - if (isFloatingInterestRate != null && isFloatingInterestRate - && !loanProduct.getFloatingRates().isFloatingInterestRateCalculationAllowed()) { + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.isFloatingInterestRate, element)) { baseDataValidator.reset().parameter(LoanApiConstants.isFloatingInterestRate).failWithCode( - "true.not.supported.for.selected.loanproduct", - "isFloatingInterestRate value of true not supported for selected Loan Product."); + "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."); } - } 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 BigDecimal interestRatePerPeriod = this.fromApiJsonHelper + .extractBigDecimalWithLocaleNamed(LoanApiConstants.interestRatePerPeriodParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.interestRatePerPeriodParameterName).value(interestRatePerPeriod) + .notNull().zeroOrPositiveAmount(); + isInterestBearing = interestRatePerPeriod.compareTo(BigDecimal.ZERO) > 0; } - 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 { + final Integer amortizationType = this.fromApiJsonHelper + .extractIntegerSansLocaleNamed(LoanApiConstants.amortizationTypeParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.amortizationTypeParameterName).value(amortizationType).notNull() + .inMinMaxRange(0, 1); - 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."); + 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 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); + final LocalDate expectedDisbursementDate = this.fromApiJsonHelper + .extractLocalDateNamed(LoanApiConstants.expectedDisbursementDateParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.expectedDisbursementDateParameterName).value(expectedDisbursementDate) + .notNull(); - 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"); - } + // grace validation + final Integer graceOnPrincipalPayment = this.fromApiJsonHelper + .extractIntegerWithLocaleNamed(LoanApiConstants.graceOnPrincipalPaymentParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.graceOnPrincipalPaymentParameterName).value(graceOnPrincipalPayment) + .zeroOrPositiveAmount(); - 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(); - } + final Integer graceOnInterestPayment = this.fromApiJsonHelper + .extractIntegerWithLocaleNamed(LoanApiConstants.graceOnInterestPaymentParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.graceOnInterestPaymentParameterName).value(graceOnInterestPayment) + .zeroOrPositiveAmount(); - 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(); - } + final Integer graceOnInterestCharged = this.fromApiJsonHelper + .extractIntegerWithLocaleNamed(LoanApiConstants.graceOnInterestChargedParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.graceOnInterestChargedParameterName).value(graceOnInterestCharged) + .zeroOrPositiveAmount(); - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.inArrearsToleranceParameterName, element)) { - final BigDecimal inArrearsTolerance = this.fromApiJsonHelper - .extractBigDecimalWithLocaleNamed(LoanApiConstants.inArrearsToleranceParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.inArrearsToleranceParameterName).value(inArrearsTolerance).ignoreIfNull() + 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(); - } - final LocalDate submittedOnDate = this.fromApiJsonHelper.extractLocalDateNamed(LoanApiConstants.submittedOnDateParameterName, - element); + 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(); + } - baseDataValidator.reset().parameter(LoanApiConstants.submittedOnDateParameterName).value(submittedOnDate).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(); + } - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.submittedOnNoteParameterName, element)) { - final String submittedOnNote = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.submittedOnNoteParameterName, + final LocalDate submittedOnDate = this.fromApiJsonHelper.extractLocalDateNamed(LoanApiConstants.submittedOnDateParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.submittedOnNoteParameterName).value(submittedOnNote).ignoreIfNull() - .notExceedingLengthOf(500); - } - final String transactionProcessingStrategy = this.fromApiJsonHelper - .extractStringNamed(LoanApiConstants.transactionProcessingStrategyCodeParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.submittedOnDateParameterName).value(submittedOnDate).notNull(); - validateTransactionProcessingStrategy(transactionProcessingStrategy, loanProduct, baseDataValidator); + 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); + } - validateLinkedSavingsAccount(element, baseDataValidator); + final String transactionProcessingStrategy = this.fromApiJsonHelper + .extractStringNamed(LoanApiConstants.transactionProcessingStrategyCodeParameterName, element); - 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); + 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(); + if (createStandingInstructionAtDisbursement) { + baseDataValidator.reset().parameter(LoanApiConstants.linkAccountIdParameterName).value(linkAccountId).notNull() + .longGreaterThanZero(); + } } - } - // charges - loanChargeApiJsonValidator.validateLoanCharges(element, loanProduct, baseDataValidator); + // 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()) - */ + /** + * 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 (!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(); } - } else { - baseDataValidator.reset().parameter(LoanApiConstants.collateralParameterName).expectedArrayButIsNot(); } } - } - 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 (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 (isEqualAmortization) { - throw new EqualAmortizationUnsupportedFeatureException("fixed.emi", "fixed emi"); + 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(); } - 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 (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.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); - } + 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); + 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"); - } + 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(); + List allocationRules = loanProduct.getPaymentAllocationRules(); - if (LoanScheduleProcessingType.HORIZONTAL.name().equals(loanScheduleProcessingType) - && AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY.equals(transactionProcessingStrategy)) { - advancedPaymentAllocationsValidator.checkGroupingOfAllocationRules(allocationRules); - } + 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"); + 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); + validateBorrowerCycle(element, loanProduct, clientId, groupId, baseDataValidator); - loanProductDataValidator.fixedLengthValidations(transactionProcessingStrategy, isInterestBearing, numberOfRepayments, - repaymentEvery, element, 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."); + // 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); - - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } + 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 void validateForModify(final JsonCommand command, final Loan loan) { 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; - - 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(); - } + validateOrThrow("loan", baseDataValidator -> { + final JsonElement element = this.fromApiJsonHelper.parse(json); + boolean atLeastOneParameterPassedForUpdate = false; - 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"); + 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); } - } - - 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 (productId != null) { + atLeastOneParameterPassedForUpdate = true; + baseDataValidator.reset().parameter(LoanApiConstants.productIdParameterName).value(productId).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); - } + 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); + } - CalendarUtils.validateNthDayOfMonthFrequency(baseDataValidator, LoanApiConstants.repaymentFrequencyNthDayTypeParameterName, - LoanApiConstants.repaymentFrequencyDayOfWeekTypeParameterName, element, this.fromApiJsonHelper); + 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"); + } + } - 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); - } + BigDecimal fixedPrincipalPercentagePerInstallment = this.fromApiJsonHelper + .extractBigDecimalWithLocaleNamed(LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName, element); + baseDataValidator.reset().parameter(LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName) + .value(fixedPrincipalPercentagePerInstallment).notLessThanMin(BigDecimal.ONE) + .notGreaterThanMax(BigDecimal.valueOf(100)); - if (loanProduct.isLinkedToFloatingInterestRate()) { - if (isEqualAmortization) { - throw new EqualAmortizationUnsupportedFeatureException("floating.interest.rate", "floating interest rate"); + 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.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.fundIdParameterName, element)) { + atLeastOneParameterPassedForUpdate = true; + final Long fundId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.fundIdParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.fundIdParameterName).value(fundId).ignoreIfNull() + .integerGreaterThanZero(); } - Boolean isFloatingInterestRate = loan.getIsFloatingInterestRate(); - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.isFloatingInterestRate, element)) { - isFloatingInterestRate = this.fromApiJsonHelper.extractBooleanNamed(LoanApiConstants.isFloatingInterestRate, element); + 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(); } - 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); + + 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 (interestType == null) { - interestType = loan.getLoanProductRelatedDetail().getInterestMethod().getValue(); + 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"); } - 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 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(); } - BigDecimal interestRateDifferential = loan.getInterestRateDifferential(); - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.interestRateDifferentialParameterName, element)) { - interestRateDifferential = this.fromApiJsonHelper - .extractBigDecimalWithLocaleNamed(LoanApiConstants.interestRateDifferentialParameterName, element); + 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(); } - baseDataValidator.reset().parameter(LoanApiConstants.interestRateDifferentialParameterName).value(interestRateDifferential) - .notNull().zeroOrPositiveAmount().inMinAndMaxAmountRange(loanProduct.getFloatingRates().getMinDifferentialLendingRate(), - loanProduct.getFloatingRates().getMaxDifferentialLendingRate()); - } else { + 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.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.loanTermFrequencyTypeParameterName, element)) { + atLeastOneParameterPassedForUpdate = true; + final Integer loanTermFrequencyType = this.fromApiJsonHelper + .extractIntegerWithLocaleNamed(LoanApiConstants.loanTermFrequencyTypeParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.loanTermFrequencyTypeParameterName).value(loanTermFrequencyType) + .notNull().inMinMaxRange(0, 3); } - 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."); + + 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(); } - BigDecimal interestRatePerPeriod = loan.getLoanProductRelatedDetail().getNominalInterestRatePerPeriod(); - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.interestRatePerPeriodParameterName, element)) { - this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.interestRatePerPeriodParameterName, element); + 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(); } - baseDataValidator.reset().parameter(LoanApiConstants.interestRatePerPeriodParameterName).value(interestRatePerPeriod).notNull() - .zeroOrPositiveAmount(); - } + 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); + } - Integer interestCalculationPeriodType = loanProduct.getLoanProductRelatedDetail().getInterestCalculationPeriodMethod().getValue(); + CalendarUtils.validateNthDayOfMonthFrequency(baseDataValidator, LoanApiConstants.repaymentFrequencyNthDayTypeParameterName, + LoanApiConstants.repaymentFrequencyDayOfWeekTypeParameterName, element, this.fromApiJsonHelper); - 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 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); + } - 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 (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 (!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"); - } + 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); + } - LocalDate expectedDisbursementDate = loan.getExpectedDisbursementDate(); - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.expectedDisbursementDateParameterName, element)) { - atLeastOneParameterPassedForUpdate = true; + 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."); + } - final String expectedDisbursementDateStr = this.fromApiJsonHelper - .extractStringNamed(LoanApiConstants.expectedDisbursementDateParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.expectedDisbursementDateParameterName).value(expectedDisbursementDateStr) - .notBlank(); + 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()); - expectedDisbursementDate = this.fromApiJsonHelper.extractLocalDateNamed(LoanApiConstants.expectedDisbursementDateParameterName, - element); - baseDataValidator.reset().parameter(LoanApiConstants.expectedDisbursementDateParameterName).value(expectedDisbursementDate) - .notNull(); - } + } else { - 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(); - } + 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."); + } - 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(); - } + 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(); - 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(); - } + Integer interestCalculationPeriodType = loanProduct.getLoanProductRelatedDetail().getInterestCalculationPeriodMethod() + .getValue(); - 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.interestCalculationPeriodTypeParameterName, element)) { + atLeastOneParameterPassedForUpdate = true; + interestCalculationPeriodType = this.fromApiJsonHelper + .extractIntegerWithLocaleNamed(LoanApiConstants.interestCalculationPeriodTypeParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.interestCalculationPeriodTypeParameterName) + .value(interestCalculationPeriodType).notNull().inMinMaxRange(0, 1); + } - 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"); + 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 (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 (!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"); + } - 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); - } + LocalDate expectedDisbursementDate = loan.getExpectedDisbursementDate(); + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.expectedDisbursementDateParameterName, element)) { + atLeastOneParameterPassedForUpdate = true; - validateLinkedSavingsAccount(element, baseDataValidator); + final String expectedDisbursementDateStr = this.fromApiJsonHelper + .extractStringNamed(LoanApiConstants.expectedDisbursementDateParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.expectedDisbursementDateParameterName) + .value(expectedDisbursementDateStr).notBlank(); - // charges - loanChargeApiJsonValidator.validateLoanCharges(element, loanProduct, baseDataValidator); + expectedDisbursementDate = this.fromApiJsonHelper + .extractLocalDateNamed(LoanApiConstants.expectedDisbursementDateParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.expectedDisbursementDateParameterName).value(expectedDisbursementDate) + .notNull(); + } - final String loanTypeStr = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.loanTypeParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.loanTypeParameterName).value(loanTypeStr).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(); + } - if (!StringUtils.isBlank(loanTypeStr)) { - final AccountType loanType = AccountType.fromName(loanTypeStr); + 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(); + } - if (loanType.isInvalid()) { - baseDataValidator.reset().parameter(LoanApiConstants.loanTypeParameterName).value(loanType.getValue()) - .isOneOfEnumValues(AccountType.class); + 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 (!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 (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(); + } - final Type collateralParameterTypeOfMap = new TypeToken>() { + 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(); + } - }.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 (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 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 (!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 (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 (isEqualAmortization) { - throw new EqualAmortizationUnsupportedFeatureException("fixed.emi", "fixed emi"); + + 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(); } - 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 (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 (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 (isTopup != null && isTopup) { + final Long loanId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.loanIdToClose, element); + baseDataValidator.reset().parameter(LoanApiConstants.loanIdToClose).value(loanId).notNull().longGreaterThanZero(); - 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."); + } - 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); + 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); - 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"); - } + 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"); + } - List allocationRules = loanProduct.getPaymentAllocationRules(); + List allocationRules = loanProduct.getPaymentAllocationRules(); - if (LoanScheduleProcessingType.HORIZONTAL.name().equals(loanScheduleProcessingType) - && AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY.equals(transactionProcessingStrategy)) { - advancedPaymentAllocationsValidator.checkGroupingOfAllocationRules(allocationRules); - } + 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); - validateClientOrGroup(client, group, productId); + validateDisbursementDetails(loanProduct, element); + validateSubmittedOnDate(element, loan.getSubmittedOnDate(), loanProduct); - // 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); + validateClientOrGroup(client, group, productId); - Integer recurringMoratoriumOnPrincipalPeriods = loan.getLoanProductRelatedDetail().getRecurringMoratoriumOnPrincipalPeriods(); - if (this.fromApiJsonHelper.parameterExists("recurringMoratoriumOnPrincipalPeriods", element)) { - recurringMoratoriumOnPrincipalPeriods = this.fromApiJsonHelper - .extractIntegerWithLocaleNamed("recurringMoratoriumOnPrincipalPeriods", element); - } - validateBorrowerCycle(element, loanProduct, clientId, groupId, baseDataValidator); + // 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); - loanProductDataValidator.validateRepaymentPeriodWithGraceSettings(numberOfRepayments, graceOnPrincipalPayment, - graceOnInterestPayment, graceOnInterestCharged, recurringMoratoriumOnPrincipalPeriods, baseDataValidator); + Integer recurringMoratoriumOnPrincipalPeriods = loan.getLoanProductRelatedDetail().getRecurringMoratoriumOnPrincipalPeriods(); + if (this.fromApiJsonHelper.parameterExists("recurringMoratoriumOnPrincipalPeriods", element)) { + recurringMoratoriumOnPrincipalPeriods = this.fromApiJsonHelper + .extractIntegerWithLocaleNamed("recurringMoratoriumOnPrincipalPeriods", element); + } + validateBorrowerCycle(element, loanProduct, clientId, groupId, 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 void validateForUndo(final String json) { }.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); - - 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); - } + validateOrThrow(LOANAPPLICATION_UNDO, baseDataValidator -> { + final JsonElement element = this.fromApiJsonHelper.parse(json); - 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) { - - 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); - } + validateOrThrow("loan", baseDataValidator -> { + 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); + } + }); } private void validateLoanTermAndRepaidEveryValues(final Integer loanTermFrequency, final Integer loanTermFrequencyType, @@ -1490,10 +1486,10 @@ private void validateLoanTermAndRepaidEveryValues(final Integer loanTermFrequenc 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(); @@ -1584,6 +1580,12 @@ private void validateDisbursementsAreDatewiseOrdered(JsonElement element, final } } + 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); @@ -1649,23 +1651,16 @@ public void validateLoanMultiDisbursementDate(final JsonElement element, final D .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, @@ -1877,4 +1872,44 @@ private void validateForSupportedParameters(String json) { }.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 995a1b5c471..07bb8d59f2b 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 @@ -105,6 +105,8 @@ import org.springframework.orm.jpa.JpaSystemException; import org.springframework.transaction.annotation.Transactional; +import static org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType.SAME_AS_REPAYMENT_PERIOD; + @Slf4j @RequiredArgsConstructor public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements LoanApplicationWritePlatformService { @@ -236,28 +238,21 @@ private void createAndPersistCalendarInstanceForInterestRecalculation(final Loan 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(),