From 92e291a9e433222695223f291544ee32c985860e Mon Sep 17 00:00:00 2001 From: Adam Saghy Date: Thu, 6 Jun 2024 17:10:45 +0200 Subject: [PATCH] FINERACT-2090: Restructure Loan creation and modification --- .../charge/domain/ChargeTimeType.java | 36 +- .../loanaccount/api/LoanApiConstants.java | 3 +- .../command/LoanChargeCommand.java | 42 +- .../data/LoanCollateralManagementData.java | 18 +- .../portfolio/loanaccount/domain/Loan.java | 541 ++----- .../loanaccount/domain/LoanCharge.java | 5 - .../domain/LoanApplicationTerms.java | 10 +- .../loanaccount/mapper/LoanChargeMapper.java | 44 + .../LoanCollateralManagementMapper.java | 39 + .../loanproduct/domain/LoanProduct.java | 8 +- ...MinimumRepaymentScheduleRelatedDetail.java | 10 +- .../domain/LoanProductRelatedDetail.java | 263 +--- ...tatableChecksWritePlatformServiceImpl.java | 4 +- .../domain/LoanAccountDomainServiceJpa.java | 3 +- .../service/LoanScheduleAssembler.java | 314 +++- ...cheduleCalculationPlatformServiceImpl.java | 107 +- ...per.java => LoanApplicationValidator.java} | 815 ++++++++--- .../LoanChargeApiJsonValidator.java | 175 +++ .../LoanEventApiJsonValidator.java | 10 +- ...Helper.java => LoanScheduleValidator.java} | 6 +- .../GLIMAccountInfoWritePlatformService.java | 4 +- ...IMAccountInfoWritePlatformServiceImpl.java | 13 +- ...WritePlatformServiceJpaRepositoryImpl.java | 1304 ++++------------- .../loanaccount/service/LoanAssembler.java | 772 +++++++--- .../service/LoanChargeAssembler.java | 34 +- .../LoanChargeWritePlatformServiceImpl.java | 3 +- .../LoanDisbursementDetailsAssembler.java | 59 + .../loanaccount/service/LoanUtilService.java | 54 - ...WritePlatformServiceJpaRepositoryImpl.java | 17 +- .../starter/LoanAccountConfiguration.java | 108 +- .../LoanProductDataValidator.java | 93 +- ...ntAllocationLoanRepaymentScheduleTest.java | 22 +- .../ClientLoanIntegrationTest.java | 6 +- .../LoanApplicationApprovalTest.java | 2 +- 34 files changed, 2458 insertions(+), 2486 deletions(-) create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanChargeMapper.java create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanCollateralManagementMapper.java rename fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/{LoanApplicationCommandFromApiJsonHelper.java => LoanApplicationValidator.java} (67%) rename fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/{CalculateLoanScheduleQueryFromApiJsonHelper.java => LoanScheduleValidator.java} (98%) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDisbursementDetailsAssembler.java diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/charge/domain/ChargeTimeType.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/charge/domain/ChargeTimeType.java index 6b932b3ff..8b07fa651 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/charge/domain/ChargeTimeType.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/charge/domain/ChargeTimeType.java @@ -143,47 +143,51 @@ public enum ChargeTimeType { } public boolean isTimeOfDisbursement() { - return ChargeTimeType.DISBURSEMENT.getValue().equals(this.value); + return ChargeTimeType.DISBURSEMENT.equals(this); } public boolean isOnSpecifiedDueDate() { - return this.value.equals(ChargeTimeType.SPECIFIED_DUE_DATE.getValue()); + return this.equals(ChargeTimeType.SPECIFIED_DUE_DATE); } public boolean isSavingsActivation() { - return this.value.equals(ChargeTimeType.SAVINGS_ACTIVATION.getValue()); + return this.equals(ChargeTimeType.SAVINGS_ACTIVATION); } public boolean isSavingsClosure() { - return this.value.equals(ChargeTimeType.SAVINGS_CLOSURE.getValue()); + return this.equals(ChargeTimeType.SAVINGS_CLOSURE); } public boolean isWithdrawalFee() { - return this.value.equals(ChargeTimeType.WITHDRAWAL_FEE.getValue()); + return this.equals(ChargeTimeType.WITHDRAWAL_FEE); } public boolean isSavingsNoActivityFee() { - return this.value.equals(ChargeTimeType.SAVINGS_NOACTIVITY_FEE.getValue()); + return this.equals(ChargeTimeType.SAVINGS_NOACTIVITY_FEE); } public boolean isAnnualFee() { - return this.value.equals(ChargeTimeType.ANNUAL_FEE.getValue()); + return this.equals(ChargeTimeType.ANNUAL_FEE); } public boolean isMonthlyFee() { - return this.value.equals(ChargeTimeType.MONTHLY_FEE.getValue()); + return this.equals(ChargeTimeType.MONTHLY_FEE); } public boolean isWeeklyFee() { - return this.value.equals(ChargeTimeType.WEEKLY_FEE.getValue()); + return this.equals(ChargeTimeType.WEEKLY_FEE); } public boolean isInstalmentFee() { - return this.value.equals(ChargeTimeType.INSTALMENT_FEE.getValue()); + return this.equals(ChargeTimeType.INSTALMENT_FEE); + } + + public boolean isSpecifiedDueDate() { + return this.equals(ChargeTimeType.SPECIFIED_DUE_DATE); } public boolean isOverdueInstallment() { - return this.value.equals(ChargeTimeType.OVERDUE_INSTALLMENT.getValue()); + return this.equals(ChargeTimeType.OVERDUE_INSTALLMENT); } public boolean isAllowedLoanChargeTime() { @@ -200,22 +204,22 @@ public enum ChargeTimeType { } public boolean isOverdraftFee() { - return this.value.equals(ChargeTimeType.OVERDRAFT_FEE.getValue()); + return this.equals(ChargeTimeType.OVERDRAFT_FEE); } public boolean isTrancheDisbursement() { - return this.value.equals(ChargeTimeType.TRANCHE_DISBURSEMENT.getValue()); + return this.equals(ChargeTimeType.TRANCHE_DISBURSEMENT); } public boolean isShareAccountActivation() { - return this.value.equals(ChargeTimeType.SHAREACCOUNT_ACTIVATION.getValue()); + return this.equals(ChargeTimeType.SHAREACCOUNT_ACTIVATION); } public boolean isSharesPurchase() { - return this.value.equals(ChargeTimeType.SHARE_PURCHASE.getValue()); + return this.equals(ChargeTimeType.SHARE_PURCHASE); } public boolean isSharesRedeem() { - return this.value.equals(ChargeTimeType.SHARE_REDEEM.getValue()); + return this.equals(ChargeTimeType.SHARE_REDEEM); } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java index a439a5fbf..f2cb9a7cd 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java @@ -20,7 +20,7 @@ package org.apache.fineract.portfolio.loanaccount.api; public interface LoanApiConstants { - String emiAmountParameterName = "fixedEmiAmount"; + String fixedEmiAmountParameterName = "fixedEmiAmount"; String maxOutstandingBalanceParameterName = "maxOutstandingLoanBalance"; String disbursementDataParameterName = "disbursementData"; String expectedDisbursementDateParameterName = "expectedDisbursementDate"; @@ -36,6 +36,7 @@ public interface LoanApiConstants { String amountParameterName = "amount"; String chargeTimeTypeParameterName = "chargeTimeType"; String chargeCalculationTypeParameterName = "chargeCalculationType"; + String chargePaymentModeParameterName = "chargePaymentMode"; String principalDisbursedParameterName = "transactionAmount"; String chargesParameterName = "charges"; String loanIdTobeApproved = "loanId"; diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/command/LoanChargeCommand.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/command/LoanChargeCommand.java index a135c5ab5..08e46bc71 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/command/LoanChargeCommand.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/command/LoanChargeCommand.java @@ -18,44 +18,30 @@ */ package org.apache.fineract.portfolio.loanaccount.command; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.io.Serial; +import java.io.Serializable; import java.math.BigDecimal; import java.time.LocalDate; +import lombok.Data; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; /** * Java object representation of {@link LoanCharge} API JSON. */ -public class LoanChargeCommand implements Comparable { +@Data +public class LoanChargeCommand implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; @SuppressWarnings("unused") - private final Long id; - private final Long chargeId; - private final BigDecimal amount; + private Long id; + private Long chargeId; + private BigDecimal amount; @SuppressWarnings("unused") - private final Integer chargeTimeType; + private Integer chargeTimeType; @SuppressWarnings("unused") - private final Integer chargeCalculationType; + private Integer chargeCalculationType; @SuppressWarnings("unused") - private final LocalDate dueDate; - - public LoanChargeCommand(final Long id, final Long chargeId, final BigDecimal amount, final Integer chargeTimeType, - final Integer chargeCalculationType, final LocalDate dueDate) { - this.id = id; - this.chargeId = chargeId; - this.amount = amount; - this.chargeTimeType = chargeTimeType; - this.chargeCalculationType = chargeCalculationType; - this.dueDate = dueDate; - } - - @Override - @SuppressFBWarnings(value = "EQ_COMPARETO_USE_OBJECT_EQUALS", justification = "TODO: fix this! See: https://stackoverflow.com/questions/2609037/findbugs-how-to-solve-eq-compareto-use-object-equals") - public int compareTo(final LoanChargeCommand o) { - int comparison = this.chargeId.compareTo(o.chargeId); - if (comparison == 0) { - comparison = this.amount.compareTo(o.amount); - } - return comparison; - } + private LocalDate dueDate; } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanCollateralManagementData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanCollateralManagementData.java index bbce1a3c0..bed684423 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanCollateralManagementData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanCollateralManagementData.java @@ -19,20 +19,20 @@ package org.apache.fineract.portfolio.loanaccount.data; import java.math.BigDecimal; -import lombok.Getter; -import lombok.RequiredArgsConstructor; +import lombok.AllArgsConstructor; +import lombok.Data; -@RequiredArgsConstructor -@Getter +@Data +@AllArgsConstructor public class LoanCollateralManagementData { - private final Long clientCollateralId; + private Long clientCollateralId; - private final BigDecimal quantity; + private BigDecimal quantity; - private final BigDecimal total; + private BigDecimal total; - private final BigDecimal totalCollateral; + private BigDecimal totalCollateral; - private final Long id; + private Long id; } 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 3043db27f..1d07b282e 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,8 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.Predicate; +import lombok.Getter; +import lombok.Setter; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.infrastructure.codes.domain.CodeValue; import org.apache.fineract.infrastructure.configuration.service.TemporaryConfigurationServiceContainer; @@ -104,10 +106,8 @@ import org.apache.fineract.portfolio.floatingrates.data.FloatingRatePeriodData; import org.apache.fineract.portfolio.fund.domain.Fund; import org.apache.fineract.portfolio.group.domain.Group; import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; -import org.apache.fineract.portfolio.loanaccount.command.LoanChargeCommand; import org.apache.fineract.portfolio.loanaccount.data.DisbursementData; import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; -import org.apache.fineract.portfolio.loanaccount.data.LoanCollateralManagementData; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; @@ -128,7 +128,6 @@ import org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementData import org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementDataRequiredException; import org.apache.fineract.portfolio.loanaccount.exception.UndoLastTrancheDisbursementException; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO; -import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.AprCalculator; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; @@ -153,6 +152,8 @@ import org.apache.fineract.useradministration.domain.AppUser; @Entity @Table(name = "m_loan", uniqueConstraints = { @UniqueConstraint(columnNames = { "account_no" }, name = "loan_account_no_UNIQUE"), @UniqueConstraint(columnNames = { "external_id" }, name = "loan_externalid_UNIQUE") }) +@Setter +@Getter public class Loan extends AbstractAuditableWithUTCDateTimeCustom { public static final String RECALCULATE_LOAN_SCHEDULE = "recalculateLoanSchedule"; @@ -475,62 +476,73 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { @Column(name = "enable_installment_level_delinquency", nullable = false) private boolean enableInstallmentLevelDelinquency = false; - public static Loan newIndividualLoanApplication(final String accountNo, final Client client, final Integer loanType, + public static Loan newIndividualLoanApplication(final String accountNo, final Client client, final AccountType loanType, final LoanProduct loanProduct, final Fund fund, final Staff officer, final CodeValue loanPurpose, - final String transactionProcessingStrategyCode, final LoanProductRelatedDetail loanRepaymentScheduleDetail, - final Set loanCharges, final Set collateral, final BigDecimal fixedEmiAmount, + final LoanRepaymentScheduleTransactionProcessor transactionProcessingStrategy, + final LoanProductRelatedDetail loanRepaymentScheduleDetail, final Set loanCharges, + final Set collateral, final BigDecimal fixedEmiAmount, final List disbursementDetails, final BigDecimal maxOutstandingLoanBalance, final Boolean createStandingInstructionAtDisbursement, final Boolean isFloatingInterestRate, - final BigDecimal interestRateDifferential, final List rates, final BigDecimal fixedPrincipalPercentagePerInstallment) { - return new Loan(accountNo, client, null, loanType, fund, officer, loanPurpose, transactionProcessingStrategyCode, loanProduct, + final BigDecimal interestRateDifferential, final List rates, final BigDecimal fixedPrincipalPercentagePerInstallment, + final ExternalId externalId, final LoanApplicationTerms loanApplicationTerms, final LoanScheduleModel loanScheduleModel, + final Boolean enableInstallmentLevelDelinquency, final LocalDate submittedOnDate) { + return new Loan(accountNo, client, null, loanType, fund, officer, loanPurpose, transactionProcessingStrategy, loanProduct, loanRepaymentScheduleDetail, null, loanCharges, collateral, null, fixedEmiAmount, disbursementDetails, maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, isFloatingInterestRate, interestRateDifferential, rates, - fixedPrincipalPercentagePerInstallment); + fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms, loanScheduleModel, + enableInstallmentLevelDelinquency, submittedOnDate); } - public static Loan newGroupLoanApplication(final String accountNo, final Group group, final Integer loanType, + public static Loan newGroupLoanApplication(final String accountNo, final Group group, final AccountType loanType, final LoanProduct loanProduct, final Fund fund, final Staff officer, final CodeValue loanPurpose, - final String transactionProcessingStrategyCode, final LoanProductRelatedDetail loanRepaymentScheduleDetail, - final Set loanCharges, final Set collateral, final Boolean syncDisbursementWithMeeting, - final BigDecimal fixedEmiAmount, final List disbursementDetails, - final BigDecimal maxOutstandingLoanBalance, final Boolean createStandingInstructionAtDisbursement, - final Boolean isFloatingInterestRate, final BigDecimal interestRateDifferential, final List rates, - final BigDecimal fixedPrincipalPercentagePerInstallment) { - return new Loan(accountNo, null, group, loanType, fund, officer, loanPurpose, transactionProcessingStrategyCode, loanProduct, - loanRepaymentScheduleDetail, null, loanCharges, collateral, syncDisbursementWithMeeting, fixedEmiAmount, - disbursementDetails, maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, isFloatingInterestRate, - interestRateDifferential, rates, fixedPrincipalPercentagePerInstallment); + final LoanRepaymentScheduleTransactionProcessor transactionProcessingStrategy, + final LoanProductRelatedDetail loanRepaymentScheduleDetail, final Set loanCharges, + final Boolean syncDisbursementWithMeeting, final BigDecimal fixedEmiAmount, + final List disbursementDetails, final BigDecimal maxOutstandingLoanBalance, + final Boolean createStandingInstructionAtDisbursement, final Boolean isFloatingInterestRate, + final BigDecimal interestRateDifferential, final List rates, final BigDecimal fixedPrincipalPercentagePerInstallment, + final ExternalId externalId, final LoanApplicationTerms loanApplicationTerms, final LoanScheduleModel loanScheduleModel, + final Boolean enableInstallmentLevelDelinquency, final LocalDate submittedOnDate) { + return new Loan(accountNo, null, group, loanType, fund, officer, loanPurpose, transactionProcessingStrategy, loanProduct, + loanRepaymentScheduleDetail, null, loanCharges, null, syncDisbursementWithMeeting, fixedEmiAmount, disbursementDetails, + maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, isFloatingInterestRate, interestRateDifferential, rates, + fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms, loanScheduleModel, + enableInstallmentLevelDelinquency, submittedOnDate); } public static Loan newIndividualLoanApplicationFromGroup(final String accountNo, final Client client, final Group group, - final Integer loanType, final LoanProduct loanProduct, final Fund fund, final Staff officer, final CodeValue loanPurpose, - final String transactionProcessingStrategyCode, final LoanProductRelatedDetail loanRepaymentScheduleDetail, - final Set loanCharges, final Set collateral, final Boolean syncDisbursementWithMeeting, - final BigDecimal fixedEmiAmount, final List disbursementDetails, - final BigDecimal maxOutstandingLoanBalance, final Boolean createStandingInstructionAtDisbursement, - final Boolean isFloatingInterestRate, final BigDecimal interestRateDifferential, final List rates, - final BigDecimal fixedPrincipalPercentagePerInstallment) { - return new Loan(accountNo, client, group, loanType, fund, officer, loanPurpose, transactionProcessingStrategyCode, loanProduct, - loanRepaymentScheduleDetail, null, loanCharges, collateral, syncDisbursementWithMeeting, fixedEmiAmount, - disbursementDetails, maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, isFloatingInterestRate, - interestRateDifferential, rates, fixedPrincipalPercentagePerInstallment); + final AccountType loanType, final LoanProduct loanProduct, final Fund fund, final Staff officer, final CodeValue loanPurpose, + final LoanRepaymentScheduleTransactionProcessor transactionProcessingStrategy, + final LoanProductRelatedDetail loanRepaymentScheduleDetail, final Set loanCharges, + final Boolean syncDisbursementWithMeeting, final BigDecimal fixedEmiAmount, + final List disbursementDetails, final BigDecimal maxOutstandingLoanBalance, + final Boolean createStandingInstructionAtDisbursement, final Boolean isFloatingInterestRate, + final BigDecimal interestRateDifferential, final List rates, final BigDecimal fixedPrincipalPercentagePerInstallment, + final ExternalId externalId, final LoanApplicationTerms loanApplicationTerms, final LoanScheduleModel loanScheduleModel, + final Boolean enableInstallmentLevelDelinquency, final LocalDate submittedOnDate) { + return new Loan(accountNo, client, group, loanType, fund, officer, loanPurpose, transactionProcessingStrategy, loanProduct, + loanRepaymentScheduleDetail, null, loanCharges, null, syncDisbursementWithMeeting, fixedEmiAmount, disbursementDetails, + maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, isFloatingInterestRate, interestRateDifferential, rates, + fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms, loanScheduleModel, + enableInstallmentLevelDelinquency, submittedOnDate); } protected Loan() { this.client = null; } - private Loan(final String accountNo, final Client client, final Group group, final Integer loanType, final Fund fund, - final Staff loanOfficer, final CodeValue loanPurpose, final String transactionProcessingStrategyCode, - final LoanProduct loanProduct, final LoanProductRelatedDetail loanRepaymentScheduleDetail, final LoanStatus loanStatus, - final Set loanCharges, final Set collateral, final Boolean syncDisbursementWithMeeting, - final BigDecimal fixedEmiAmount, final List disbursementDetails, - final BigDecimal maxOutstandingLoanBalance, final Boolean createStandingInstructionAtDisbursement, - final Boolean isFloatingInterestRate, final BigDecimal interestRateDifferential, final List rates, - final BigDecimal fixedPrincipalPercentagePerInstallment) { + private Loan(final String accountNo, final Client client, final Group group, final AccountType loanType, final Fund fund, + final Staff loanOfficer, final CodeValue loanPurpose, + final LoanRepaymentScheduleTransactionProcessor transactionProcessingStrategy, final LoanProduct loanProduct, + final LoanProductRelatedDetail loanRepaymentScheduleDetail, final LoanStatus loanStatus, final Set loanCharges, + final Set collateral, final Boolean syncDisbursementWithMeeting, final BigDecimal fixedEmiAmount, + final List disbursementDetails, final BigDecimal maxOutstandingLoanBalance, + final Boolean createStandingInstructionAtDisbursement, final Boolean isFloatingInterestRate, + final BigDecimal interestRateDifferential, final List rates, final BigDecimal fixedPrincipalPercentagePerInstallment, + final ExternalId externalId, final LoanApplicationTerms loanApplicationTerms, final LoanScheduleModel loanScheduleModel, + final Boolean enableInstallmentLevelDelinquency, final LocalDate submittedOnDate) { this.loanRepaymentScheduleDetail = loanRepaymentScheduleDetail; - this.loanRepaymentScheduleDetail.validateRepaymentPeriodWithGraceSettings(); this.isFloatingInterestRate = isFloatingInterestRate; this.interestRateDifferential = interestRateDifferential; @@ -543,12 +555,14 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } this.client = client; this.group = group; - this.loanType = loanType; + this.loanType = loanType.getValue(); this.fund = fund; this.loanOfficer = loanOfficer; this.loanPurpose = loanPurpose; - this.transactionProcessingStrategyCode = transactionProcessingStrategyCode; + this.transactionProcessingStrategyCode = transactionProcessingStrategy.getCode(); + this.transactionProcessingStrategyName = transactionProcessingStrategy.getName(); + this.loanProduct = loanProduct; if (loanStatus != null) { this.loanStatus = loanStatus.getValue(); @@ -563,7 +577,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { this.summary = new LoanSummary(); } - if (loanType.equals(1) && collateral != null && !collateral.isEmpty()) { + if (loanType.isIndividualAccount() && collateral != null && !collateral.isEmpty()) { this.loanCollateralManagements = associateWithThisLoan(collateral); } else { this.loanCollateralManagements = null; @@ -573,7 +587,12 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { this.syncDisbursementWithMeeting = syncDisbursementWithMeeting; this.fixedEmiAmount = fixedEmiAmount; this.maxOutstandingLoanBalance = maxOutstandingLoanBalance; - this.disbursementDetails = disbursementDetails; + if (disbursementDetails != null) { + this.disbursementDetails = disbursementDetails; + for (final LoanDisbursementDetails loanDisbursementDetails : disbursementDetails) { + loanDisbursementDetails.updateLoan(this); + } + } this.approvedPrincipal = this.loanRepaymentScheduleDetail.getPrincipal().getAmount(); this.createStandingInstructionAtDisbursement = createStandingInstructionAtDisbursement; @@ -590,7 +609,26 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { // Add net get net disbursal amount from charges and principal this.netDisbursalAmount = this.approvedPrincipal.subtract(deriveSumTotalOfChargesDueAtDisbursement()); + this.loanStatus = LoanStatus.SUBMITTED_AND_PENDING_APPROVAL.getValue(); + this.externalId = externalId; + this.termFrequency = loanApplicationTerms.getLoanTermFrequency(); + this.termPeriodFrequencyType = loanApplicationTerms.getLoanTermPeriodFrequencyType().getValue(); + this.expectedDisbursementDate = loanApplicationTerms.getExpectedDisbursementDate(); + this.expectedFirstRepaymentOnDate = loanApplicationTerms.getRepaymentStartFromDate(); + this.interestChargedFromDate = loanApplicationTerms.getInterestChargedFromDate(); + this.submittedOnDate = submittedOnDate != null ? submittedOnDate : DateUtils.getBusinessLocalDate(); + updateLoanSchedule(loanScheduleModel); + + updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement()); + + // Copy interest recalculation settings if interest recalculation is enabled + if (this.loanRepaymentScheduleDetail.isInterestRecalculationEnabled()) { + this.loanInterestRecalculationDetails = LoanInterestRecalculationDetails + .createFrom(this.loanProduct.getProductInterestRecalculationDetails()); + this.loanInterestRecalculationDetails.updateLoan(this); + } + this.enableInstallmentLevelDelinquency = enableInstallmentLevelDelinquency; } public Integer getNumberOfRepayments() { @@ -652,10 +690,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return this.accountNumberRequiresAutoGeneration; } - public void setAccountNumberRequiresAutoGeneration(final boolean accountNumberRequiresAutoGeneration) { - this.accountNumberRequiresAutoGeneration = accountNumberRequiresAutoGeneration; - } - public void addLoanCharge(final LoanCharge loanCharge) { if (isChargesAdditionAllowed() && loanCharge.isDueAtDisbursement()) { @@ -1460,274 +1494,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { doPostLoanTransactionChecks(getLastUserTransactionDate(), loanLifecycleStateMachine); } - public Map loanApplicationModification(final JsonCommand command, final Set possiblyModifedLoanCharges, - final Set possiblyModifiedLoanCollateralItems, final AprCalculator aprCalculator, - boolean isChargesModified, final LoanProduct loanProduct) { - - final Map actualChanges = this.loanRepaymentScheduleDetail.updateLoanApplicationAttributes(command, aprCalculator); - final MonetaryCurrency currency = new MonetaryCurrency(loanProduct.getCurrency().getCode(), - loanProduct.getCurrency().getDigitsAfterDecimal(), loanProduct.getCurrency().getCurrencyInMultiplesOf()); - this.loanRepaymentScheduleDetail.updateCurrency(currency); - - if (!actualChanges.isEmpty()) { - final boolean recalculateLoanSchedule = !(actualChanges.size() == 1 && actualChanges.containsKey(IN_ARREARS_TOLERANCE)); - actualChanges.put(RECALCULATE_LOAN_SCHEDULE, recalculateLoanSchedule); - isChargesModified = true; - } - - final String dateFormatAsInput = command.dateFormat(); - final String localeAsInput = command.locale(); - - final String accountNoParamName = ACCOUNT_NO; - if (command.isChangeInStringParameterNamed(accountNoParamName, this.accountNumber)) { - final String newValue = command.stringValueOfParameterNamed(accountNoParamName); - actualChanges.put(accountNoParamName, newValue); - this.accountNumber = StringUtils.defaultIfEmpty(newValue, null); - } - - if (command.isChangeInBooleanParameterNamed(CREATE_STANDING_INSTRUCTION_AT_DISBURSEMENT, - shouldCreateStandingInstructionAtDisbursement())) { - final Boolean valueAsInput = command.booleanObjectValueOfParameterNamed(CREATE_STANDING_INSTRUCTION_AT_DISBURSEMENT); - actualChanges.put(CREATE_STANDING_INSTRUCTION_AT_DISBURSEMENT, valueAsInput); - this.createStandingInstructionAtDisbursement = valueAsInput; - } - - if (command.isChangeInStringParameterNamed(EXTERNAL_ID, this.externalId.getValue())) { - final String newValue = command.stringValueOfParameterNamed(EXTERNAL_ID); - ExternalId externalId = ExternalIdFactory.produce(newValue); - if (externalId.isEmpty() && TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled()) { - externalId = ExternalId.generate(); - } - actualChanges.put(EXTERNAL_ID, externalId); - this.externalId = externalId; - } - - // add clientId, groupId and loanType changes to actual changes - - final Long clientId = this.client == null ? null : this.client.getId(); - if (command.isChangeInLongParameterNamed(CLIENT_ID, clientId)) { - final Long newValue = command.longValueOfParameterNamed(CLIENT_ID); - actualChanges.put(CLIENT_ID, newValue); - } - - // FIXME: AA - We may require separate api command to move loan from one - // group to another - final Long groupId = this.group == null ? null : this.group.getId(); - if (command.isChangeInLongParameterNamed(GROUP_ID, groupId)) { - final Long newValue = command.longValueOfParameterNamed(GROUP_ID); - actualChanges.put(GROUP_ID, newValue); - } - - if (command.isChangeInLongParameterNamed(PRODUCT_ID, this.loanProduct.getId())) { - final Long newValue = command.longValueOfParameterNamed(PRODUCT_ID); - actualChanges.put(PRODUCT_ID, newValue); - actualChanges.put(RECALCULATE_LOAN_SCHEDULE, true); - } - - if (command.isChangeInBooleanParameterNamed(IS_FLOATING_INTEREST_RATE, this.isFloatingInterestRate)) { - final Boolean newValue = command.booleanObjectValueOfParameterNamed(IS_FLOATING_INTEREST_RATE); - actualChanges.put(IS_FLOATING_INTEREST_RATE, newValue); - this.isFloatingInterestRate = newValue; - } - - if (command.isChangeInBigDecimalParameterNamed(INTEREST_RATE_DIFFERENTIAL, this.interestRateDifferential)) { - final BigDecimal newValue = command.bigDecimalValueOfParameterNamed(INTEREST_RATE_DIFFERENTIAL); - actualChanges.put(INTEREST_RATE_DIFFERENTIAL, newValue); - this.interestRateDifferential = newValue; - } - - Long existingFundId = null; - if (this.fund != null) { - existingFundId = this.fund.getId(); - } - if (command.isChangeInLongParameterNamed(FUND_ID, existingFundId)) { - final Long newValue = command.longValueOfParameterNamed(FUND_ID); - actualChanges.put(FUND_ID, newValue); - } - - Long existingLoanOfficerId = null; - if (this.loanOfficer != null) { - existingLoanOfficerId = this.loanOfficer.getId(); - } - - if (command.isChangeInLongParameterNamed(LOAN_OFFICER_ID, existingLoanOfficerId)) { - final Long newValue = command.longValueOfParameterNamed(LOAN_OFFICER_ID); - actualChanges.put(LOAN_OFFICER_ID, newValue); - } - - Long existingLoanPurposeId = null; - if (this.loanPurpose != null) { - existingLoanPurposeId = this.loanPurpose.getId(); - } - - if (command.isChangeInLongParameterNamed(LOAN_PURPOSE_ID, existingLoanPurposeId)) { - final Long newValue = command.longValueOfParameterNamed(LOAN_PURPOSE_ID); - actualChanges.put(LOAN_PURPOSE_ID, newValue); - } - - if (command.isChangeInStringParameterNamed(TRANSACTION_PROCESSING_STRATEGY_CODE, transactionProcessingStrategyCode)) { - final String newValue = command.stringValueOfParameterNamed(TRANSACTION_PROCESSING_STRATEGY_CODE); - actualChanges.put(TRANSACTION_PROCESSING_STRATEGY_CODE, newValue); - } - - if (command.isChangeInLocalDateParameterNamed(SUBMITTED_ON_DATE, getSubmittedOnDate())) { - final String valueAsInput = command.stringValueOfParameterNamed(SUBMITTED_ON_DATE); - actualChanges.put(SUBMITTED_ON_DATE, valueAsInput); - actualChanges.put(DATE_FORMAT, dateFormatAsInput); - actualChanges.put(LOCALE, localeAsInput); - - this.submittedOnDate = command.localDateValueOfParameterNamed(SUBMITTED_ON_DATE); - } - - if (command.isChangeInLocalDateParameterNamed(EXPECTED_DISBURSEMENT_DATE, getExpectedDisbursedOnLocalDate())) { - final String valueAsInput = command.stringValueOfParameterNamed(EXPECTED_DISBURSEMENT_DATE); - actualChanges.put(EXPECTED_DISBURSEMENT_DATE, valueAsInput); - actualChanges.put(DATE_FORMAT, dateFormatAsInput); - actualChanges.put(LOCALE, localeAsInput); - actualChanges.put(RECALCULATE_LOAN_SCHEDULE, true); - - this.expectedDisbursementDate = command.localDateValueOfParameterNamed(EXPECTED_DISBURSEMENT_DATE); - removeFirstDisbursementTransaction(); - } - - if (command.isChangeInLocalDateParameterNamed(REPAYMENTS_STARTING_FROM_DATE, getExpectedFirstRepaymentOnDate())) { - final String valueAsInput = command.stringValueOfParameterNamed(REPAYMENTS_STARTING_FROM_DATE); - actualChanges.put(REPAYMENTS_STARTING_FROM_DATE, valueAsInput); - actualChanges.put(DATE_FORMAT, dateFormatAsInput); - actualChanges.put(LOCALE, localeAsInput); - actualChanges.put(RECALCULATE_LOAN_SCHEDULE, true); - - this.expectedFirstRepaymentOnDate = command.localDateValueOfParameterNamed(REPAYMENTS_STARTING_FROM_DATE); - } - - if (command.isChangeInBooleanParameterNamed(SYNC_DISBURSEMENT_WITH_MEETING, isSyncDisbursementWithMeeting())) { - final Boolean valueAsInput = command.booleanObjectValueOfParameterNamed(SYNC_DISBURSEMENT_WITH_MEETING); - actualChanges.put(SYNC_DISBURSEMENT_WITH_MEETING, valueAsInput); - this.syncDisbursementWithMeeting = valueAsInput; - } - - if (command.isChangeInLocalDateParameterNamed(INTEREST_CHARGED_FROM_DATE, getInterestChargedFromDate())) { - final String valueAsInput = command.stringValueOfParameterNamed(INTEREST_CHARGED_FROM_DATE); - actualChanges.put(INTEREST_CHARGED_FROM_DATE, valueAsInput); - actualChanges.put(DATE_FORMAT, dateFormatAsInput); - actualChanges.put(LOCALE, localeAsInput); - actualChanges.put(RECALCULATE_LOAN_SCHEDULE, true); - - this.interestChargedFromDate = command.localDateValueOfParameterNamed(INTEREST_CHARGED_FROM_DATE); - } - - // the comparison should be done with the tenant date - // (DateUtils.getBusinessDate()) and not the server date (new - // LocalDate()) - if (DateUtils.isDateInTheFuture(getSubmittedOnDate())) { - final String errorMessage = "The date on which a loan is submitted cannot be in the future."; - throw new InvalidLoanStateTransitionException("submittal", "cannot.be.a.future.date", errorMessage, getSubmittedOnDate()); - } - - if (this.client != null && DateUtils.isBefore(getSubmittedOnDate(), this.client.getActivationDate())) { - final String errorMessage = "The date on which a loan is submitted cannot be earlier than client's activation date."; - throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.client.activation.date", errorMessage, - getSubmittedOnDate()); - } else if (this.group != null && DateUtils.isBefore(getSubmittedOnDate(), this.group.getActivationDate())) { - final String errorMessage = "The date on which a loan is submitted cannot be earlier than groups's activation date."; - throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.group.activation.date", errorMessage, - getSubmittedOnDate()); - } - - if (DateUtils.isAfter(getSubmittedOnDate(), getExpectedDisbursedOnLocalDate())) { - final String errorMessage = "The date on which a loan is submitted cannot be after its expected disbursement date: " - + getExpectedDisbursedOnLocalDate().toString(); - throw new InvalidLoanStateTransitionException("submittal", "cannot.be.after.expected.disbursement.date", errorMessage, - getSubmittedOnDate(), getExpectedDisbursedOnLocalDate()); - } - - if (isChargesModified) { - actualChanges.put(PARAM_CHARGES, getLoanCharges(possiblyModifedLoanCharges)); - actualChanges.put(RECALCULATE_LOAN_SCHEDULE, true); - } - - if (command.parameterExists(PARAM_COLLATERAL) && possiblyModifiedLoanCollateralItems != null - && !possiblyModifiedLoanCollateralItems.isEmpty()) { - Set loanCollateralManagements = this.loanCollateralManagements; - boolean isTrue = possiblyModifiedLoanCollateralItems.equals(loanCollateralManagements); - - if (!isTrue) { - actualChanges.put(PARAM_COLLATERAL, getLoanCollateralDataFormCommand(possiblyModifiedLoanCollateralItems)); - } - } - - if (command.isChangeInIntegerParameterNamed(LOAN_TERM_FREQUENCY, this.termFrequency)) { - final Integer newValue = command.integerValueOfParameterNamed(LOAN_TERM_FREQUENCY); - actualChanges.put(LOAN_TERM_FREQUENCY, newValue); - this.termFrequency = newValue; - } - - if (command.isChangeInIntegerParameterNamed(LOAN_TERM_FREQUENCY_TYPE, this.termPeriodFrequencyType)) { - final Integer newValue = command.integerValueOfParameterNamed(LOAN_TERM_FREQUENCY_TYPE); - final PeriodFrequencyType newTermPeriodFrequencyType = PeriodFrequencyType.fromInt(newValue); - actualChanges.put(LOAN_TERM_FREQUENCY_TYPE, newTermPeriodFrequencyType.getValue()); - this.termPeriodFrequencyType = newValue; - } - - if (command.isChangeInBigDecimalParameterNamed(PRINCIPAL, this.approvedPrincipal)) { - this.approvedPrincipal = command.bigDecimalValueOfParameterNamed(PRINCIPAL); - } - - if (command.isChangeInBigDecimalParameterNamed(PRINCIPAL, this.proposedPrincipal)) { - this.proposedPrincipal = command.bigDecimalValueOfParameterNamed(PRINCIPAL); - } - - if (loanProduct.isMultiDisburseLoan()) { - updateDisbursementDetails(command, actualChanges); - if (command.isChangeInBigDecimalParameterNamed(LoanApiConstants.maxOutstandingBalanceParameterName, - this.maxOutstandingLoanBalance)) { - this.maxOutstandingLoanBalance = command - .bigDecimalValueOfParameterNamed(LoanApiConstants.maxOutstandingBalanceParameterName); - } - final JsonArray disbursementDataArray = command.arrayOfParameterNamed(LoanApiConstants.disbursementDataParameterName); - - if (loanProduct.isDisallowExpectedDisbursements()) { - if (disbursementDataArray != null && !disbursementDataArray.isEmpty()) { - final String errorMessage = "For this loan product, disbursement details are not allowed"; - throw new MultiDisbursementDataNotAllowedException(LoanApiConstants.disbursementDataParameterName, errorMessage); - } - } else { - if (disbursementDataArray == null || disbursementDataArray.size() == 0) { - final String errorMessage = "For this loan product, disbursement details must be provided"; - throw new MultiDisbursementDataRequiredException(LoanApiConstants.disbursementDataParameterName, errorMessage); - } - - if (disbursementDataArray.size() > loanProduct.maxTrancheCount()) { - final String errorMessage = "Number of tranche shouldn't be greter than " + loanProduct.maxTrancheCount(); - throw new ExceedingTrancheCountException(LoanApiConstants.disbursementDataParameterName, errorMessage, - loanProduct.maxTrancheCount(), disbursementDetails.size()); - } - } - } else { - this.disbursementDetails.clear(); - } - - if (loanProduct.isMultiDisburseLoan() || loanProduct.isCanDefineInstallmentAmount()) { - if (command.isChangeInBigDecimalParameterNamed(LoanApiConstants.emiAmountParameterName, this.fixedEmiAmount)) { - this.fixedEmiAmount = command.bigDecimalValueOfParameterNamed(LoanApiConstants.emiAmountParameterName); - actualChanges.put(LoanApiConstants.emiAmountParameterName, this.fixedEmiAmount); - actualChanges.put(RECALCULATE_LOAN_SCHEDULE, true); - } - } else { - this.fixedEmiAmount = null; - } - - if (command.isChangeInBigDecimalParameterNamed(LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName, - this.fixedPrincipalPercentagePerInstallment)) { - this.fixedPrincipalPercentagePerInstallment = command - .bigDecimalValueOfParameterNamed(LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName); - actualChanges.put(LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName, - this.fixedPrincipalPercentagePerInstallment); - } - - return actualChanges; - } - public void recalculateAllCharges() { Set charges = this.getActiveCharges(); int penaltyWaitPeriod = 0; @@ -2068,112 +1834,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return list; } - private LoanCollateralManagementData[] getLoanCollateralDataFormCommand(final Set setOfLoanCollateral) { - - LoanCollateralManagementData[] existingLoanCollateral = null; - - final List loanCollateralList = new ArrayList<>(); - for (final LoanCollateralManagement loanCollateral : setOfLoanCollateral) { - - loanCollateralList.add(loanCollateral.toCommand()); - - } - - existingLoanCollateral = loanCollateralList.toArray(new LoanCollateralManagementData[0]); - - return existingLoanCollateral; - } - - private LoanChargeCommand[] getLoanCharges(final Set setOfLoanCharges) { - - LoanChargeCommand[] existingLoanCharges = null; - - final List loanChargesList = new ArrayList<>(); - for (final LoanCharge loanCharge : setOfLoanCharges) { - loanChargesList.add(loanCharge.toCommand()); - } - - existingLoanCharges = loanChargesList.toArray(new LoanChargeCommand[0]); - - return existingLoanCharges; - } - - private void removeFirstDisbursementTransaction() { - List transactions = getLoanTransactions(); - for (final LoanTransaction loanTransaction : transactions) { - if (loanTransaction.isDisbursement()) { - removeLoanTransaction(loanTransaction); - break; - } - } - } - - public void loanApplicationSubmittal(final LoanScheduleModel loanSchedule, final LoanApplicationTerms loanApplicationTerms, - final LoanLifecycleStateMachine lifecycleStateMachine, final LocalDate submittedOn, final ExternalId externalId, - final boolean allowTransactionsOnHoliday, final List holidays, final WorkingDays workingDays, - final boolean allowTransactionsOnNonWorkingDay) { - updateLoanSchedule(loanSchedule); - - lifecycleStateMachine.transition(LoanEvent.LOAN_CREATED, this); - - this.externalId = externalId; - this.termFrequency = loanApplicationTerms.getLoanTermFrequency(); - this.termPeriodFrequencyType = loanApplicationTerms.getLoanTermPeriodFrequencyType().getValue(); - this.submittedOnDate = submittedOn; - this.expectedDisbursementDate = loanApplicationTerms.getExpectedDisbursementDate(); - this.expectedFirstRepaymentOnDate = loanApplicationTerms.getRepaymentStartFromDate(); - this.interestChargedFromDate = loanApplicationTerms.getInterestChargedFromDate(); - - updateLoanScheduleDependentDerivedFields(); - - if (DateUtils.isDateInTheFuture(submittedOn)) { - final String errorMessage = "The date on which a loan is submitted cannot be in the future."; - throw new InvalidLoanStateTransitionException("submittal", "cannot.be.a.future.date", errorMessage, submittedOn, - DateUtils.getBusinessLocalDate()); - } - - if (this.client != null && this.client.isActivatedAfter(submittedOn)) { - final String errorMessage = "The date on which a loan is submitted cannot be earlier than client's activation date."; - throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.client.activation.date", errorMessage, submittedOn, - client.getActivationDate()); - } - - validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_CREATED, submittedOn); - - if (this.group != null && this.group.isActivatedAfter(submittedOn)) { - final String errorMessage = "The date on which a loan is submitted cannot be earlier than groups's activation date."; - throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.group.activation.date", errorMessage, submittedOn, - group.getActivationDate()); - } - - if (DateUtils.isAfter(submittedOn, getExpectedDisbursedOnLocalDate())) { - final String errorMessage = "The date on which a loan is submitted cannot be after its expected disbursement date: " - + getExpectedDisbursedOnLocalDate().toString(); - throw new InvalidLoanStateTransitionException("submittal", "cannot.be.after.expected.disbursement.date", errorMessage, - submittedOn, getExpectedDisbursedOnLocalDate()); - } - - // charges are optional - int penaltyWaitPeriod = 0; - for (final LoanCharge loanCharge : getActiveCharges()) { - recalculateLoanCharge(loanCharge, penaltyWaitPeriod); - } - - updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement()); - - // validate if disbursement date is a holiday or a non-working day - validateDisbursementDateIsOnNonWorkingDay(workingDays, allowTransactionsOnNonWorkingDay); - validateDisbursementDateIsOnHoliday(allowTransactionsOnHoliday, holidays); - - // Copy interest recalculation settings if interest recalculation is enabled - if (this.loanRepaymentScheduleDetail.isInterestRecalculationEnabled()) { - this.loanInterestRecalculationDetails = LoanInterestRecalculationDetails - .createFrom(this.loanProduct.getProductInterestRecalculationDetails()); - this.loanInterestRecalculationDetails.updateLoan(this); - } - - } - private LocalDate determineExpectedMaturityDate() { List installments = getRepaymentScheduleInstallments().stream() .filter(i -> !i.isDownPayment() && !i.isAdditional()).toList(); @@ -4876,7 +4536,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { break; } return dueRepaymentPeriodDate.minusDays(1);// get 2n-1 range date from - // startDate + // startDate } private void validateDisbursementDateIsOnNonWorkingDay(final WorkingDays workingDays, final boolean allowTransactionsOnNonWorkingDay) { @@ -4945,7 +4605,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } public void updateInterestRateFrequencyType() { - this.loanRepaymentScheduleDetail.updateInterestPeriodFrequencyType(this.loanProduct.getInterestPeriodFrequencyType()); + this.loanRepaymentScheduleDetail.setInterestPeriodFrequencyType(this.loanProduct.getInterestPeriodFrequencyType()); } public Integer getTermFrequency() { @@ -4975,14 +4635,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { this.loanStatus = loanStatus; } - public void validateExpectedDisbursementForHolidayAndNonWorkingDay(final WorkingDays workingDays, - final boolean allowTransactionsOnHoliday, final List holidays, final boolean allowTransactionsOnNonWorkingDay) { - // validate if disbursement date is a holiday or a non-working day - validateDisbursementDateIsOnNonWorkingDay(workingDays, allowTransactionsOnNonWorkingDay); - validateDisbursementDateIsOnHoliday(allowTransactionsOnHoliday, holidays); - - } - private void validateActivityNotBeforeClientOrGroupTransferDate(final LoanEvent event, final LocalDate activityDate) { if (this.client != null && this.client.getOfficeJoiningDate() != null) { final LocalDate clientOfficeJoiningDate = this.client.getOfficeJoiningDate(); @@ -4991,11 +4643,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { String action = null; String postfix = null; switch (event) { - case LOAN_CREATED: - errorMessage = "The date on which a loan is submitted cannot be earlier than client's transfer date to this office"; - action = "submittal"; - postfix = "cannot.be.before.client.transfer.date"; - break; 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"; @@ -5400,6 +5047,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return currentDisbursementDetails; } + public void clearDisbursementDetails() { + this.disbursementDetails.clear(); + } + public LoanDisbursementDetails getDisbursementDetails(final LocalDate transactionDate, final BigDecimal transactionAmount) { for (LoanDisbursementDetails disbursementDetail : this.disbursementDetails) { if (!disbursementDetail.isReversed() && disbursementDetail.getDisbursementDate().equals(transactionDate) @@ -5525,7 +5176,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } public void updateIsInterestRecalculationEnabled() { - this.loanRepaymentScheduleDetail.updateIsInterestRecalculationEnabled(isInterestRecalculationEnabledForProduct()); + this.loanRepaymentScheduleDetail.setInterestRecalculationEnabled(isInterestRecalculationEnabledForProduct()); } public LoanInterestRecalculationDetails loanInterestRecalculationDetails() { @@ -6196,11 +5847,11 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { // loanCalendar = loanCalendarInstance.getCalendar(); // } - final Integer graceOnPrincipalPayment = this.loanRepaymentScheduleDetail.graceOnPrincipalPayment(); - final Integer graceOnInterestPayment = this.loanRepaymentScheduleDetail.graceOnInterestPayment(); - final Integer graceOnInterestCharged = this.loanRepaymentScheduleDetail.graceOnInterestCharged(); + final Integer graceOnPrincipalPayment = this.loanRepaymentScheduleDetail.getGraceOnPrincipalPayment(); + final Integer graceOnInterestPayment = this.loanRepaymentScheduleDetail.getGraceOnInterestPayment(); + final Integer graceOnInterestCharged = this.loanRepaymentScheduleDetail.getGraceOnInterestCharged(); final LocalDate interestChargedFromDate = getInterestChargedFromDate(); - final Integer graceOnArrearsAgeing = this.loanRepaymentScheduleDetail.getGraceOnDueDate(); + final Integer graceOnArrearsAgeing = this.loanRepaymentScheduleDetail.getGraceOnArrearsAgeing(); final Money inArrearsToleranceMoney = this.loanRepaymentScheduleDetail.getInArrearsTolerance(); final BigDecimal emiAmount = getFixedEmiAmount(); @@ -6258,7 +5909,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } public void updateNumberOfRepayments(Integer numberOfRepayments) { - this.loanRepaymentScheduleDetail.updateNumberOfRepayments(numberOfRepayments); + this.loanRepaymentScheduleDetail.setNumberOfRepayments(numberOfRepayments); } public void updateRescheduledOnDate(LocalDate rescheduledOnDate) { @@ -7134,8 +6785,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return rates; } - public Integer getLoanType() { - return loanType; + public AccountType getLoanType() { + return AccountType.fromInt(loanType); } public void setLoanType(Integer loanType) { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java index 42ec041a6..f81471b04 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java @@ -54,7 +54,6 @@ import org.apache.fineract.portfolio.charge.domain.ChargeCalculationType; import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode; import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; import org.apache.fineract.portfolio.charge.exception.LoanChargeWithoutMandatoryFieldException; -import org.apache.fineract.portfolio.loanaccount.command.LoanChargeCommand; import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData; import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidDetail; import org.apache.fineract.portfolio.loanaccount.data.LoanInstallmentChargeData; @@ -509,10 +508,6 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom { return value.compareTo(BigDecimal.ZERO) > 0; } - public LoanChargeCommand toCommand() { - return new LoanChargeCommand(getId(), this.charge.getId(), this.amount, this.chargeTime, this.chargeCalculation, getDueLocalDate()); - } - public LocalDate getDueLocalDate() { return this.dueDate; } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java index 380292d46..41f8a95d2 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java @@ -306,10 +306,10 @@ public final class LoanApplicationTerms { final Money principalMoney = loanProductRelatedDetail.getPrincipal(); // - final Integer graceOnPrincipalPayment = loanProductRelatedDetail.graceOnPrincipalPayment(); - final Integer recurringMoratoriumOnPrincipalPeriods = loanProductRelatedDetail.recurringMoratoriumOnPrincipalPeriods(); - final Integer graceOnInterestPayment = loanProductRelatedDetail.graceOnInterestPayment(); - final Integer graceOnInterestCharged = loanProductRelatedDetail.graceOnInterestCharged(); + final Integer graceOnPrincipalPayment = loanProductRelatedDetail.getGraceOnPrincipalPayment(); + final Integer recurringMoratoriumOnPrincipalPeriods = loanProductRelatedDetail.getRecurringMoratoriumOnPrincipalPeriods(); + final Integer graceOnInterestPayment = loanProductRelatedDetail.getGraceOnInterestPayment(); + final Integer graceOnInterestCharged = loanProductRelatedDetail.getGraceOnInterestCharged(); // Interest recalculation settings final DaysInMonthType daysInMonthType = loanProductRelatedDetail.fetchDaysInMonthType(); @@ -334,7 +334,7 @@ public final class LoanApplicationTerms { repaymentsStartingFromDate, calculatedRepaymentsStartingFromDate, graceOnPrincipalPayment, recurringMoratoriumOnPrincipalPeriods, graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate, inArrearsTolerance, multiDisburseLoan, emiAmount, disbursementDatas, maxOutstandingBalance, - loanProductRelatedDetail.getGraceOnDueDate(), daysInMonthType, daysInYearType, isInterestRecalculationEnabled, + loanProductRelatedDetail.getGraceOnArrearsAgeing(), daysInMonthType, daysInYearType, isInterestRecalculationEnabled, rescheduleStrategyMethod, compoundingMethod, restCalendarInstance, recalculationFrequencyType, compoundingCalendarInstance, compoundingFrequencyType, principalThresholdForLastInstalment, installmentAmountInMultiplesOf, loanPreClosureInterestCalculationStrategy, loanCalendar, approvedAmount, loanTermVariations, calendarHistoryDataWrapper, diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanChargeMapper.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanChargeMapper.java new file mode 100644 index 000000000..9b92eda4f --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanChargeMapper.java @@ -0,0 +1,44 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.mapper; + +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.portfolio.loanaccount.command.LoanChargeCommand; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper(config = MapstructMapperConfig.class) +public interface LoanChargeMapper { + + @Mapping(target = "id", source = "loanCharge.id") + @Mapping(target = "chargeId", source = "loanCharge.charge.id") + @Mapping(target = "amount", expression = "java(loanCharge.getAmount(currency).getAmount())") + @Mapping(target = "chargeTimeType", source = "loanCharge.chargeTimeType.value") + @Mapping(target = "chargeCalculationType", source = "loanCharge.chargeCalculation.value") + @Mapping(target = "dueDate", source = "loanCharge.dueDate") + LoanChargeCommand map(LoanCharge loanCharge, MonetaryCurrency currency); + + default Set map(Set loanCharge, MonetaryCurrency currency) { + return loanCharge.stream().map(lc -> map(lc, currency)).collect(Collectors.toSet()); + } +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanCollateralManagementMapper.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanCollateralManagementMapper.java new file mode 100644 index 000000000..b591a6a75 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanCollateralManagementMapper.java @@ -0,0 +1,39 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.mapper; + +import java.util.Set; +import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig; +import org.apache.fineract.portfolio.loanaccount.data.LoanCollateralManagementData; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagement; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper(config = MapstructMapperConfig.class) +public interface LoanCollateralManagementMapper { + + @Mapping(target = "id", source = "id") + @Mapping(target = "clientCollateralId", source = "clientCollateralManagement.id") + @Mapping(target = "quantity", source = "quantity") + @Mapping(target = "total", ignore = true) + @Mapping(target = "totalCollateral", ignore = true) + LoanCollateralManagementData map(LoanCollateralManagement loanCollateralManagement); + + Set map(Set loanCollateralManagements); +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java index 337a51810..b7d7191a5 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java @@ -725,8 +725,6 @@ public class LoanProduct extends AbstractPersistableCustom { isInterestRecalculationEnabled, isEqualAmortization, enableDownPayment, disbursedAmountPercentageForDownPayment, enableAutoRepaymentForDownPayment, loanScheduleType, loanScheduleProcessingType, fixedLength); - this.loanProductRelatedDetail.validateRepaymentPeriodWithGraceSettings(); - this.loanProductMinMaxConstraints = new LoanProductMinMaxConstraints(defaultMinPrincipal, defaultMaxPrincipal, defaultMinNominalInterestRatePerPeriod, defaultMaxNominalInterestRatePerPeriod, defaultMinNumberOfInstallments, defaultMaxNumberOfInstallments); @@ -1298,21 +1296,21 @@ public class LoanProduct extends AbstractPersistableCustom { this.loanProductRelatedDetail.isEnableDownPayment())) { final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.ENABLE_DOWN_PAYMENT); actualChanges.put(LoanProductConstants.ENABLE_DOWN_PAYMENT, newValue); - this.loanProductRelatedDetail.updateEnableDownPayment(newValue); + this.loanProductRelatedDetail.setEnableDownPayment(newValue); } if (command.isChangeInBigDecimalParameterNamed(LoanProductConstants.DISBURSED_AMOUNT_PERCENTAGE_DOWN_PAYMENT, this.loanProductRelatedDetail.getDisbursedAmountPercentageForDownPayment())) { BigDecimal newValue = command.bigDecimalValueOfParameterNamed(LoanProductConstants.DISBURSED_AMOUNT_PERCENTAGE_DOWN_PAYMENT); actualChanges.put(LoanProductConstants.DISBURSED_AMOUNT_PERCENTAGE_DOWN_PAYMENT, newValue); - this.loanProductRelatedDetail.updateDisbursedAmountPercentageForDownPayment(newValue); + this.loanProductRelatedDetail.setDisbursedAmountPercentageForDownPayment(newValue); } if (command.isChangeInBooleanParameterNamed(LoanProductConstants.ENABLE_AUTO_REPAYMENT_DOWN_PAYMENT, this.loanProductRelatedDetail.isEnableAutoRepaymentForDownPayment())) { final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.ENABLE_AUTO_REPAYMENT_DOWN_PAYMENT); actualChanges.put(LoanProductConstants.ENABLE_AUTO_REPAYMENT_DOWN_PAYMENT, newValue); - this.loanProductRelatedDetail.updateEnableAutoRepaymentForDownPayment(newValue); + this.loanProductRelatedDetail.setEnableAutoRepaymentForDownPayment(newValue); } if (command.isChangeInIntegerParameterNamed(LoanProductConstants.REPAYMENT_START_DATE_TYPE, diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductMinimumRepaymentScheduleRelatedDetail.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductMinimumRepaymentScheduleRelatedDetail.java index 81c7dfd93..5ec2acdaa 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductMinimumRepaymentScheduleRelatedDetail.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductMinimumRepaymentScheduleRelatedDetail.java @@ -32,13 +32,13 @@ public interface LoanProductMinimumRepaymentScheduleRelatedDetail { Money getPrincipal(); - Integer graceOnInterestCharged(); + Integer getGraceOnInterestCharged(); - Integer graceOnInterestPayment(); + Integer getGraceOnInterestPayment(); - Integer graceOnPrincipalPayment(); + Integer getGraceOnPrincipalPayment(); - Integer recurringMoratoriumOnPrincipalPeriods(); + Integer getRecurringMoratoriumOnPrincipalPeriods(); Money getInArrearsTolerance(); @@ -60,5 +60,5 @@ public interface LoanProductMinimumRepaymentScheduleRelatedDetail { AmortizationMethod getAmortizationMethod(); - Integer getGraceOnDueDate(); + Integer getGraceOnArrearsAgeing(); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java index 6c74c7534..c7cea895a 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java @@ -24,14 +24,11 @@ import jakarta.persistence.Embedded; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; import java.math.BigDecimal; -import java.util.ArrayList; import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; +import lombok.Getter; +import lombok.Setter; import org.apache.fineract.infrastructure.core.api.JsonCommand; -import org.apache.fineract.infrastructure.core.data.ApiParameterError; -import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; -import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.common.domain.DaysInMonthType; @@ -48,6 +45,8 @@ import org.apache.fineract.portfolio.loanproduct.LoanProductConstants; * a {@link Loan}. */ @Embeddable +@Getter +@Setter public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentScheduleRelatedDetail { @Embedded @@ -241,82 +240,32 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche return Money.of(this.currency, this.principal); } - public void setPrincipal(BigDecimal principal) { - this.principal = principal; - } - - @Override - public Integer graceOnInterestCharged() { - return this.graceOnInterestCharged; - } - - @Override - public Integer graceOnInterestPayment() { - return this.graceOnInterestPayment; - } - - @Override - public Integer graceOnPrincipalPayment() { - return this.graceOnPrincipalPayment; - } - - @Override - public Integer recurringMoratoriumOnPrincipalPeriods() { - return this.recurringMoratoriumOnPrincipalPeriods; - } - @Override public Money getInArrearsTolerance() { return Money.of(this.currency, this.inArrearsTolerance); } + // TODO: REVIEW @Override public BigDecimal getNominalInterestRatePerPeriod() { return this.nominalInterestRatePerPeriod == null ? null : BigDecimal.valueOf(Double.parseDouble(this.nominalInterestRatePerPeriod.stripTrailingZeros().toString())); } + // TODO: REVIEW @Override public PeriodFrequencyType getInterestPeriodFrequencyType() { return this.interestPeriodFrequencyType == null ? PeriodFrequencyType.INVALID : this.interestPeriodFrequencyType; } + // TODO: REVIEW @Override public BigDecimal getAnnualNominalInterestRate() { return this.annualNominalInterestRate == null ? null : BigDecimal.valueOf(Double.parseDouble(this.annualNominalInterestRate.stripTrailingZeros().toString())); } - @Override - public InterestMethod getInterestMethod() { - return this.interestMethod; - } - - @Override - public InterestCalculationPeriodMethod getInterestCalculationPeriodMethod() { - return this.interestCalculationPeriodMethod; - } - - @Override - public Integer getRepayEvery() { - return this.repayEvery; - } - - @Override - public PeriodFrequencyType getRepaymentPeriodFrequencyType() { - return this.repaymentPeriodFrequencyType; - } - - @Override - public Integer getNumberOfRepayments() { - return this.numberOfRepayments; - } - - @Override - public AmortizationMethod getAmortizationMethod() { - return this.amortizationMethod; - } - + // TODO: Move into assembler public Map update(final JsonCommand command, final AprCalculator aprCalculator) { final Map actualChanges = new LinkedHashMap<>(20); @@ -367,18 +316,6 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche loanScheduleProcessingType = newLoanScheduleProcessingType; } - final Map loanApplicationAttributeChanges = updateLoanApplicationAttributes(command, aprCalculator); - - actualChanges.putAll(loanApplicationAttributeChanges); - - return actualChanges; - } - - public Map updateLoanApplicationAttributes(final JsonCommand command, final AprCalculator aprCalculator) { - final Map actualChanges = new LinkedHashMap<>(20); - - final String localeAsInput = command.locale(); - final String principalParamName = "principal"; if (command.isChangeInBigDecimalParameterNamed(principalParamName, this.principal)) { final BigDecimal newValue = command.bigDecimalValueOfParameterNamed(principalParamName); @@ -557,63 +494,10 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche this.isEqualAmortization = newValue; } - validateRepaymentPeriodWithGraceSettings(); - return actualChanges; } - public void updateCurrency(final MonetaryCurrency currency) { - this.currency = currency; - } - - public void validateRepaymentPeriodWithGraceSettings() { - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loanproduct"); - - if (this.numberOfRepayments <= defaultToZeroIfNull(this.graceOnPrincipalPayment)) { - baseDataValidator.reset().parameter("graceOnPrincipalPayment").value(this.graceOnPrincipalPayment) - .failWithCode(".mustBeLessThan.numberOfRepayments"); - } - - if (this.numberOfRepayments <= defaultToZeroIfNull(this.graceOnInterestPayment)) { - baseDataValidator.reset().parameter("graceOnInterestPayment").value(this.graceOnInterestPayment) - .failWithCode(".mustBeLessThan.numberOfRepayments"); - } - - if (this.numberOfRepayments < defaultToZeroIfNull(this.graceOnInterestCharged)) { - baseDataValidator.reset().parameter("graceOnInterestCharged").value(this.graceOnInterestCharged) - .failWithCode(".mustBeLessThan.numberOfRepayments"); - } - - int graceOnPrincipal = 0; - if (this.getGraceOnPrincipalPayment() != null) { - graceOnPrincipal = this.getGraceOnPrincipalPayment(); - } - int recurMoratoriumOnPrincipal = 0; - if (this.recurringMoratoriumOnPrincipalPeriods() != null) { - recurMoratoriumOnPrincipal = this.recurringMoratoriumOnPrincipalPeriods(); - } - - if ((recurMoratoriumOnPrincipal > 0) && ((this.numberOfRepayments - graceOnPrincipal) % (recurMoratoriumOnPrincipal + 1) != 1)) { - baseDataValidator.reset().parameter("graceOnPrincipalPayments.and.recurringMoratoriumOnPrincipalPeriods") - .value(graceOnPrincipal).value(recurMoratoriumOnPrincipal) - .failWithCode("causes.principal.moratorium.for.last.installment"); - } - - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - private Integer defaultToZeroIfNull(final Integer value) { - Integer result = value; - if (value == null) { - result = 0; - } - return result; - } - - private void updateInterestRateDerivedFields(final AprCalculator aprCalculator) { + public void updateInterestRateDerivedFields(final AprCalculator aprCalculator) { this.annualNominalInterestRate = aprCalculator.calculateFrom(this.interestPeriodFrequencyType, this.nominalInterestRatePerPeriod, this.numberOfRepayments, this.repayEvery, this.repaymentPeriodFrequencyType); @@ -623,15 +507,6 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche return this.currency.getCode().equalsIgnoreCase(currencyCode); } - public void updateInterestPeriodFrequencyType(final PeriodFrequencyType interestPeriodFrequencyType) { - this.interestPeriodFrequencyType = interestPeriodFrequencyType; - } - - @Override - public Integer getGraceOnDueDate() { - return this.graceOnArrearsAgeing; - } - public DaysInMonthType fetchDaysInMonthType() { return DaysInMonthType.fromInt(this.daysInMonthType); } @@ -640,129 +515,9 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche return DaysInYearType.fromInt(this.daysInYearType); } - public boolean isInterestRecalculationEnabled() { - return this.isInterestRecalculationEnabled; - } - - public void updateIsInterestRecalculationEnabled(final boolean isInterestRecalculationEnabled) { - this.isInterestRecalculationEnabled = isInterestRecalculationEnabled; - } - - public void updateNumberOfRepayments(Integer numberOfRepayments) { - this.numberOfRepayments = numberOfRepayments; - } - - public Integer getGraceOnPrincipalPayment() { - return graceOnPrincipalPayment; - } - - public void setGraceOnPrincipalPayment(Integer graceOnPrincipalPayment) { - this.graceOnPrincipalPayment = graceOnPrincipalPayment; - } - - public Integer getGraceOnInterestPayment() { - return graceOnInterestPayment; - } - - public void setGraceOnInterestPayment(Integer graceOnInterestPayment) { - this.graceOnInterestPayment = graceOnInterestPayment; - } - - public Integer getGraceOnArrearsAgeing() { - return graceOnArrearsAgeing; - } - - public void setGraceOnArrearsAgeing(Integer graceOnArrearsAgeing) { - this.graceOnArrearsAgeing = graceOnArrearsAgeing; - } - - public void setInterestMethod(InterestMethod interestMethod) { - this.interestMethod = interestMethod; - } - - public void setInterestCalculationPeriodMethod(InterestCalculationPeriodMethod interestCalculationPeriodMethod) { - this.interestCalculationPeriodMethod = interestCalculationPeriodMethod; - } - - public void setRepayEvery(Integer repayEvery) { - this.repayEvery = repayEvery; - } - - public void setRepaymentPeriodFrequencyType(PeriodFrequencyType repaymentPeriodFrequencyType) { - this.repaymentPeriodFrequencyType = repaymentPeriodFrequencyType; - } - - public void setAmortizationMethod(AmortizationMethod amortizationMethod) { - this.amortizationMethod = amortizationMethod; - } - - public void setInArrearsTolerance(BigDecimal inArrearsTolerance) { - this.inArrearsTolerance = inArrearsTolerance; - } - - public BigDecimal getArrearsTolerance() { - return this.inArrearsTolerance; - } - public void updateForFloatingInterestRates() { this.nominalInterestRatePerPeriod = null; this.interestPeriodFrequencyType = PeriodFrequencyType.INVALID; this.annualNominalInterestRate = null; } - - public boolean isAllowPartialPeriodInterestCalcualtion() { - return this.allowPartialPeriodInterestCalcualtion; - } - - public boolean isEqualAmortization() { - return isEqualAmortization; - } - - public void setEqualAmortization(boolean isEqualAmortization) { - this.isEqualAmortization = isEqualAmortization; - } - - public void setNominalInterestRatePerPeriod(BigDecimal nominalInterestRatePerPeriod) { - this.nominalInterestRatePerPeriod = nominalInterestRatePerPeriod; - } - - public boolean isEnableDownPayment() { - return enableDownPayment; - } - - public BigDecimal getDisbursedAmountPercentageForDownPayment() { - return disbursedAmountPercentageForDownPayment; - } - - public void updateEnableDownPayment(boolean enableDownPayment) { - this.enableDownPayment = enableDownPayment; - } - - public void updateDisbursedAmountPercentageForDownPayment(BigDecimal disbursedAmountPercentageForDownPayment) { - this.disbursedAmountPercentageForDownPayment = disbursedAmountPercentageForDownPayment; - } - - public Boolean isEnableAutoRepaymentForDownPayment() { - return enableAutoRepaymentForDownPayment; - } - - public void updateEnableAutoRepaymentForDownPayment(boolean enableAutoRepaymentForDownPayment) { - this.enableAutoRepaymentForDownPayment = enableAutoRepaymentForDownPayment; - } - - public LoanScheduleType getLoanScheduleType() { - return loanScheduleType; - } - - public LoanScheduleProcessingType getLoanScheduleProcessingType() { - return loanScheduleProcessingType; - } - - public Integer getFixedLength() { - return this.fixedLength; - } - - public void setFixedLength(Integer fixedLength) { - this.fixedLength = fixedLength; - } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/EntityDatatableChecksWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/EntityDatatableChecksWritePlatformServiceImpl.java index cce138b0e..e5bbc6384 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/EntityDatatableChecksWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/EntityDatatableChecksWritePlatformServiceImpl.java @@ -175,7 +175,7 @@ public class EntityDatatableChecksWritePlatformServiceImpl implements EntityData List tableRequiredBeforAction = entityDatatableChecksRepository.findByEntityStatusAndProduct(entityName, statusCode, productId); - if (tableRequiredBeforAction == null || tableRequiredBeforAction.size() < 1) { + if (tableRequiredBeforAction == null || tableRequiredBeforAction.isEmpty()) { tableRequiredBeforAction = entityDatatableChecksRepository.findByEntityStatusAndNoProduct(entityName, statusCode); } if (tableRequiredBeforAction != null) { @@ -190,7 +190,7 @@ public class EntityDatatableChecksWritePlatformServiceImpl implements EntityData reqDatatables.add(datatableName); } } - if (reqDatatables.size() > 0) { + if (!reqDatatables.isEmpty()) { throw new DatatableEntryRequiredException(reqDatatables.toString()); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java index b44af4d2d..5d6afe71e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java @@ -81,7 +81,6 @@ import org.apache.fineract.portfolio.account.domain.AccountTransferStandingInstr import org.apache.fineract.portfolio.account.domain.AccountTransferTransaction; import org.apache.fineract.portfolio.account.domain.StandingInstructionRepository; import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus; -import org.apache.fineract.portfolio.accountdetails.domain.AccountType; import org.apache.fineract.portfolio.client.domain.Client; import org.apache.fineract.portfolio.client.exception.ClientNotActiveException; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; @@ -250,7 +249,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { // changes to closed disableStandingInstructionsLinkedToClosedLoan(loan); - if (AccountType.fromInt(loan.getLoanType()).isIndividualAccount()) { + if (loan.getLoanType().isIndividualAccount()) { // Mark Post Dated Check as paid. final Set loanTransactionToRepaymentScheduleMappings = newRepaymentTransaction .getLoanTransactionToRepaymentScheduleMappings(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java index 751573146..f4a7003fd 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java @@ -34,8 +34,10 @@ import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TreeSet; +import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; +import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; @@ -100,6 +102,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanSchedul import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanaccount.serialization.VariableLoanScheduleFromApiJsonValidator; import org.apache.fineract.portfolio.loanaccount.service.LoanChargeAssembler; +import org.apache.fineract.portfolio.loanaccount.service.LoanDisbursementDetailsAssembler; import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService; import org.apache.fineract.portfolio.loanproduct.LoanProductConstants; import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod; @@ -115,10 +118,10 @@ import org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyTy import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType; import org.apache.fineract.portfolio.loanproduct.exception.LoanProductNotFoundException; import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service +@RequiredArgsConstructor public class LoanScheduleAssembler { private final FromJsonHelper fromApiJsonHelper; @@ -137,35 +140,7 @@ public class LoanScheduleAssembler { private final VariableLoanScheduleFromApiJsonValidator variableLoanScheduleFromApiJsonValidator; private final CalendarInstanceRepository calendarInstanceRepository; private final LoanUtilService loanUtilService; - - @Autowired - public LoanScheduleAssembler(final FromJsonHelper fromApiJsonHelper, final LoanProductRepository loanProductRepository, - final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository, - final LoanScheduleGeneratorFactory loanScheduleFactory, final AprCalculator aprCalculator, - final LoanChargeAssembler loanChargeAssembler, final CalendarRepository calendarRepository, - final HolidayRepository holidayRepository, final ConfigurationDomainService configurationDomainService, - final ClientRepositoryWrapper clientRepository, final GroupRepositoryWrapper groupRepository, - final WorkingDaysRepositoryWrapper workingDaysRepository, - final FloatingRatesReadPlatformService floatingRatesReadPlatformService, - final VariableLoanScheduleFromApiJsonValidator variableLoanScheduleFromApiJsonValidator, - final CalendarInstanceRepository calendarInstanceRepository, final LoanUtilService loanUtilService) { - this.fromApiJsonHelper = fromApiJsonHelper; - this.loanProductRepository = loanProductRepository; - this.applicationCurrencyRepository = applicationCurrencyRepository; - this.loanScheduleFactory = loanScheduleFactory; - this.aprCalculator = aprCalculator; - this.loanChargeAssembler = loanChargeAssembler; - this.calendarRepository = calendarRepository; - this.holidayRepository = holidayRepository; - this.configurationDomainService = configurationDomainService; - this.clientRepository = clientRepository; - this.groupRepository = groupRepository; - this.workingDaysRepository = workingDaysRepository; - this.floatingRatesReadPlatformService = floatingRatesReadPlatformService; - this.variableLoanScheduleFromApiJsonValidator = variableLoanScheduleFromApiJsonValidator; - this.calendarInstanceRepository = calendarInstanceRepository; - this.loanUtilService = loanUtilService; - } + private final LoanDisbursementDetailsAssembler loanDisbursementDetailsAssembler; public LoanApplicationTerms assembleLoanTerms(final JsonElement element) { final Long loanProductId = this.fromApiJsonHelper.extractLongNamed("productId", element); @@ -177,6 +152,15 @@ public class LoanScheduleAssembler { private LoanApplicationTerms assembleLoanApplicationTermsFrom(final JsonElement element, final LoanProduct loanProduct) { + final Boolean allowOverridingAmortization = loanProduct.getLoanConfigurableAttributes().getAmortizationBoolean(); + final Boolean allowOverridingArrearsTolerance = loanProduct.getLoanConfigurableAttributes().getArrearsToleranceBoolean(); + final Boolean allowOverridingGraceOnArrearsAging = loanProduct.getLoanConfigurableAttributes().getGraceOnArrearsAgingBoolean(); + final Boolean allowOverridingInterestCalcPeriod = loanProduct.getLoanConfigurableAttributes().getInterestCalcPeriodBoolean(); + final Boolean allowOverridingInterestMethod = loanProduct.getLoanConfigurableAttributes().getInterestMethodBoolean(); + final Boolean allowOverridingGraceOnPrincipalAndInterestPayment = loanProduct.getLoanConfigurableAttributes() + .getGraceOnPrincipalAndInterestPaymentBoolean(); + final Boolean allowOverridingRepaymentEvery = loanProduct.getLoanConfigurableAttributes().getRepaymentEveryBoolean(); + final MonetaryCurrency currency = loanProduct.getCurrency(); final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency); @@ -186,7 +170,9 @@ public class LoanScheduleAssembler { final PeriodFrequencyType loanTermPeriodFrequencyType = PeriodFrequencyType.fromInt(loanTermFrequencyType); final Integer numberOfRepayments = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("numberOfRepayments", element); - final Integer repaymentEvery = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("repaymentEvery", element); + final Integer repaymentEvery = allowOverridingRepaymentEvery + ? this.fromApiJsonHelper.extractIntegerWithLocaleNamed("repaymentEvery", element) + : loanProduct.getLoanProductRelatedDetail().getRepayEvery(); final Integer repaymentFrequencyType = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("repaymentFrequencyType", element); final PeriodFrequencyType repaymentPeriodFrequencyType = PeriodFrequencyType.fromInt(repaymentFrequencyType); final Integer nthDay = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("repaymentFrequencyNthDayType", element); @@ -194,7 +180,8 @@ public class LoanScheduleAssembler { final DayOfWeekType weekDayType = DayOfWeekType.fromInt(dayOfWeek); final Integer amortizationType = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("amortizationType", element); - final AmortizationMethod amortizationMethod = AmortizationMethod.fromInt(amortizationType); + final AmortizationMethod amortizationMethod = allowOverridingAmortization ? AmortizationMethod.fromInt(amortizationType) + : loanProduct.getLoanProductRelatedDetail().getAmortizationMethod(); boolean isEqualAmortization = false; if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.isEqualAmortizationParam, element)) { @@ -206,12 +193,14 @@ public class LoanScheduleAssembler { // interest terms final Integer interestType = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("interestType", element); - final InterestMethod interestMethod = InterestMethod.fromInt(interestType); + final InterestMethod interestMethod = allowOverridingInterestMethod ? InterestMethod.fromInt(interestType) + : loanProduct.getLoanProductRelatedDetail().getInterestMethod(); final Integer interestCalculationPeriodType = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("interestCalculationPeriodType", element); - final InterestCalculationPeriodMethod interestCalculationPeriodMethod = InterestCalculationPeriodMethod - .fromInt(interestCalculationPeriodType); + final InterestCalculationPeriodMethod interestCalculationPeriodMethod = allowOverridingInterestCalcPeriod + ? InterestCalculationPeriodMethod.fromInt(interestCalculationPeriodType) + : loanProduct.getLoanProductRelatedDetail().getInterestCalculationPeriodMethod(); Boolean allowPartialPeriodInterestCalcualtion = this.fromApiJsonHelper .extractBooleanNamed(LoanProductConstants.ALLOW_PARTIAL_PERIOD_INTEREST_CALCUALTION_PARAM_NAME, element); if (allowPartialPeriodInterestCalcualtion == null) { @@ -322,23 +311,29 @@ public class LoanScheduleAssembler { } // grace details - final Integer graceOnPrincipalPayment = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnPrincipalPayment", element); + final Integer graceOnPrincipalPayment = allowOverridingGraceOnPrincipalAndInterestPayment + ? this.fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnPrincipalPayment", element) + : loanProduct.getLoanProductRelatedDetail().getGraceOnPrincipalPayment(); final Integer recurringMoratoriumOnPrincipalPeriods = this.fromApiJsonHelper .extractIntegerWithLocaleNamed("recurringMoratoriumOnPrincipalPeriods", element); - final Integer graceOnInterestPayment = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnInterestPayment", element); + final Integer graceOnInterestPayment = allowOverridingGraceOnPrincipalAndInterestPayment + ? this.fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnInterestPayment", element) + : loanProduct.getLoanProductRelatedDetail().getGraceOnInterestPayment(); final Integer graceOnInterestCharged = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnInterestCharged", element); final LocalDate interestChargedFromDate = this.fromApiJsonHelper.extractLocalDateNamed("interestChargedFromDate", element); final Boolean isInterestChargedFromDateSameAsDisbursalDateEnabled = this.configurationDomainService .isInterestChargedFromDateSameAsDisbursementDate(); - final Integer graceOnArrearsAgeing = this.fromApiJsonHelper - .extractIntegerWithLocaleNamed(LoanProductConstants.GRACE_ON_ARREARS_AGEING_PARAMETER_NAME, element); + final Integer graceOnArrearsAgeing = allowOverridingGraceOnArrearsAging + ? this.fromApiJsonHelper.extractIntegerWithLocaleNamed(LoanProductConstants.GRACE_ON_ARREARS_AGEING_PARAMETER_NAME, element) + : loanProduct.getLoanProductRelatedDetail().getGraceOnArrearsAgeing(); // other final BigDecimal inArrearsTolerance = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("inArrearsTolerance", element); - final Money inArrearsToleranceMoney = Money.of(currency, inArrearsTolerance); + final Money inArrearsToleranceMoney = allowOverridingArrearsTolerance ? Money.of(currency, inArrearsTolerance) + : loanProduct.getLoanProductRelatedDetail().getInArrearsTolerance(); - final BigDecimal emiAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.emiAmountParameterName, + final BigDecimal emiAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.fixedEmiAmountParameterName, element); final BigDecimal maxOutstandingBalance = this.fromApiJsonHelper .extractBigDecimalWithLocaleNamed(LoanApiConstants.maxOutstandingBalanceParameterName, element); @@ -638,8 +633,8 @@ public class LoanScheduleAssembler { } } - public LoanProductRelatedDetail assembleLoanProductRelatedDetail(final JsonElement element) { - final LoanApplicationTerms loanApplicationTerms = assembleLoanTerms(element); + public LoanProductRelatedDetail assembleLoanProductRelatedDetail(final JsonElement element, final LoanProduct loanProduct) { + final LoanApplicationTerms loanApplicationTerms = assembleLoanApplicationTermsFrom(element, loanProduct); return loanApplicationTerms.toLoanProductRelatedDetail(); } @@ -671,7 +666,8 @@ public class LoanScheduleAssembler { validateDisbursementDateIsOnNonWorkingDay(loanApplicationTerms.getExpectedDisbursementDate(), workingDays); validateDisbursementDateIsOnHoliday(loanApplicationTerms.getExpectedDisbursementDate(), isHolidayEnabled, holidays); - List loanDisbursementDetails = this.loanUtilService.fetchDisbursementData(element.getAsJsonObject()); + List loanDisbursementDetails = this.loanDisbursementDetailsAssembler + .fetchDisbursementData(element.getAsJsonObject()); return assembleLoanScheduleFrom(loanApplicationTerms, isHolidayEnabled, holidays, workingDays, element, loanDisbursementDetails); } @@ -769,7 +765,7 @@ public class LoanScheduleAssembler { List installments = loan.getRepaymentScheduleInstallments(); Set dueDates = new TreeSet<>(); LocalDate graceApplicable = loan.getExpectedDisbursedOnLocalDate(); - Integer graceOnPrincipal = loan.getLoanProductRelatedDetail().graceOnPrincipalPayment(); + Integer graceOnPrincipal = loan.getLoanProductRelatedDetail().getGraceOnPrincipalPayment(); if (graceOnPrincipal == null) { graceOnPrincipal = 0; } @@ -1160,4 +1156,232 @@ public class LoanScheduleAssembler { minimumDaysBetweenDisbursalAndFirstRepayment); } } + + public void updateProductRelatedDetails(LoanProductRelatedDetail productRelatedDetail, Loan loan) { + final Boolean amortization = loan.loanProduct().getLoanConfigurableAttributes().getAmortizationBoolean(); + final Boolean arrearsTolerance = loan.loanProduct().getLoanConfigurableAttributes().getArrearsToleranceBoolean(); + final Boolean graceOnArrearsAging = loan.loanProduct().getLoanConfigurableAttributes().getGraceOnArrearsAgingBoolean(); + final Boolean interestCalcPeriod = loan.loanProduct().getLoanConfigurableAttributes().getInterestCalcPeriodBoolean(); + final Boolean interestMethod = loan.loanProduct().getLoanConfigurableAttributes().getInterestMethodBoolean(); + final Boolean graceOnPrincipalAndInterestPayment = loan.loanProduct().getLoanConfigurableAttributes() + .getGraceOnPrincipalAndInterestPaymentBoolean(); + final Boolean repaymentEvery = loan.loanProduct().getLoanConfigurableAttributes().getRepaymentEveryBoolean(); + + if (!amortization) { + productRelatedDetail.setAmortizationMethod(loan.loanProduct().getLoanProductRelatedDetail().getAmortizationMethod()); + } + if (!arrearsTolerance) { + productRelatedDetail + .setInArrearsTolerance(loan.loanProduct().getLoanProductRelatedDetail().getInArrearsTolerance().getAmount()); + } + if (!graceOnArrearsAging) { + productRelatedDetail.setGraceOnArrearsAgeing(loan.loanProduct().getLoanProductRelatedDetail().getGraceOnArrearsAgeing()); + } + if (!interestCalcPeriod) { + productRelatedDetail.setInterestCalculationPeriodMethod( + loan.loanProduct().getLoanProductRelatedDetail().getInterestCalculationPeriodMethod()); + } + if (!interestMethod) { + productRelatedDetail.setInterestMethod(loan.loanProduct().getLoanProductRelatedDetail().getInterestMethod()); + } + if (!graceOnPrincipalAndInterestPayment) { + productRelatedDetail.setGraceOnInterestPayment(loan.loanProduct().getLoanProductRelatedDetail().getGraceOnInterestPayment()); + productRelatedDetail.setGraceOnPrincipalPayment(loan.loanProduct().getLoanProductRelatedDetail().getGraceOnPrincipalPayment()); + } + if (!repaymentEvery) { + productRelatedDetail.setRepayEvery(loan.loanProduct().getLoanProductRelatedDetail().getRepayEvery()); + } + } + + public void updateLoanApplicationAttributes(JsonCommand command, Loan loan, Map changes) { + final String localeAsInput = command.locale(); + + final String principalParamName = "principal"; + LoanProductRelatedDetail loanProductRelatedDetail = loan.getLoanRepaymentScheduleDetail(); + if (command.isChangeInBigDecimalParameterNamed(principalParamName, loanProductRelatedDetail.getPrincipal().getAmount())) { + final BigDecimal newValue = command.bigDecimalValueOfParameterNamed(principalParamName); + changes.put(principalParamName, newValue); + changes.put("locale", localeAsInput); + loanProductRelatedDetail.setPrincipal(newValue); + } + + final String repaymentEveryParamName = "repaymentEvery"; + if (command.isChangeInIntegerParameterNamed(repaymentEveryParamName, loanProductRelatedDetail.getRepayEvery())) { + final Integer newValue = command.integerValueOfParameterNamed(repaymentEveryParamName); + changes.put(repaymentEveryParamName, newValue); + changes.put("locale", localeAsInput); + loanProductRelatedDetail.setRepayEvery(newValue); + } + + final String repaymentFrequencyTypeParamName = "repaymentFrequencyType"; + if (command.isChangeInIntegerParameterNamed(repaymentFrequencyTypeParamName, + loanProductRelatedDetail.getRepaymentPeriodFrequencyType().getValue())) { + Integer newValue = command.integerValueOfParameterNamed(repaymentFrequencyTypeParamName); + changes.put(repaymentFrequencyTypeParamName, newValue); + changes.put("locale", localeAsInput); + loanProductRelatedDetail.setRepaymentPeriodFrequencyType(PeriodFrequencyType.fromInt(newValue)); + } + if (PeriodFrequencyType.MONTHS.equals(loanProductRelatedDetail.getRepaymentPeriodFrequencyType())) { + final String repaymentFrequencyNthDayTypeParamName = "repaymentFrequencyNthDayType"; + Integer newValue = command.integerValueOfParameterNamed(repaymentFrequencyNthDayTypeParamName); + changes.put(repaymentFrequencyNthDayTypeParamName, newValue); + + final String repaymentFrequencyDayOfWeekTypeParamName = "repaymentFrequencyDayOfWeekType"; + newValue = command.integerValueOfParameterNamed(repaymentFrequencyDayOfWeekTypeParamName); + changes.put(repaymentFrequencyDayOfWeekTypeParamName, newValue); + changes.put("locale", localeAsInput); + } + + final String numberOfRepaymentsParamName = "numberOfRepayments"; + if (command.isChangeInIntegerParameterNamed(numberOfRepaymentsParamName, loanProductRelatedDetail.getNumberOfRepayments())) { + final Integer newValue = command.integerValueOfParameterNamed(numberOfRepaymentsParamName); + changes.put(numberOfRepaymentsParamName, newValue); + changes.put("locale", localeAsInput); + loanProductRelatedDetail.setNumberOfRepayments(newValue); + } + + final String amortizationTypeParamName = "amortizationType"; + if (command.isChangeInIntegerParameterNamed(amortizationTypeParamName, + loanProductRelatedDetail.getAmortizationMethod().getValue())) { + final Integer newValue = command.integerValueOfParameterNamed(amortizationTypeParamName); + changes.put(amortizationTypeParamName, newValue); + changes.put("locale", localeAsInput); + loanProductRelatedDetail.setAmortizationMethod(AmortizationMethod.fromInt(newValue)); + } + + final String inArrearsToleranceParamName = "inArrearsTolerance"; + if (command.isChangeInBigDecimalParameterNamed(inArrearsToleranceParamName, + loanProductRelatedDetail.getInArrearsTolerance().getAmount())) { + final BigDecimal newValue = command.bigDecimalValueOfParameterNamed(inArrearsToleranceParamName); + changes.put(inArrearsToleranceParamName, newValue); + changes.put("locale", localeAsInput); + loanProductRelatedDetail.setInArrearsTolerance(newValue); + } + + final String interestRatePerPeriodParamName = "interestRatePerPeriod"; + if (command.isChangeInBigDecimalParameterNamed(interestRatePerPeriodParamName, + loanProductRelatedDetail.getNominalInterestRatePerPeriod())) { + final BigDecimal newValue = command.bigDecimalValueOfParameterNamed(interestRatePerPeriodParamName); + changes.put(interestRatePerPeriodParamName, newValue); + changes.put("locale", localeAsInput); + loanProductRelatedDetail.setNominalInterestRatePerPeriod(newValue); + loanProductRelatedDetail.updateInterestRateDerivedFields(aprCalculator); + } + + final String interestRateFrequencyTypeParamName = "interestRateFrequencyType"; + final int interestPeriodFrequencyType = loanProductRelatedDetail.getInterestPeriodFrequencyType() == null + ? PeriodFrequencyType.INVALID.getValue() + : loanProductRelatedDetail.getInterestPeriodFrequencyType().getValue(); + if (command.isChangeInIntegerParameterNamed(interestRateFrequencyTypeParamName, interestPeriodFrequencyType)) { + final Integer newValue = command.integerValueOfParameterNamed(interestRateFrequencyTypeParamName); + changes.put(interestRateFrequencyTypeParamName, newValue); + changes.put("locale", localeAsInput); + loanProductRelatedDetail.setInterestPeriodFrequencyType(PeriodFrequencyType.fromInt(newValue)); + loanProductRelatedDetail.updateInterestRateDerivedFields(aprCalculator); + } + + final String interestTypeParamName = "interestType"; + if (command.isChangeInIntegerParameterNamed(interestTypeParamName, loanProductRelatedDetail.getInterestMethod().getValue())) { + final Integer newValue = command.integerValueOfParameterNamed(interestTypeParamName); + changes.put(interestTypeParamName, newValue); + changes.put("locale", localeAsInput); + loanProductRelatedDetail.setInterestMethod(InterestMethod.fromInt(newValue)); + } + + final String interestCalculationPeriodTypeParamName = "interestCalculationPeriodType"; + if (command.isChangeInIntegerParameterNamed(interestCalculationPeriodTypeParamName, + loanProductRelatedDetail.getInterestCalculationPeriodMethod().getValue())) { + final Integer newValue = command.integerValueOfParameterNamed(interestCalculationPeriodTypeParamName); + changes.put(interestCalculationPeriodTypeParamName, newValue); + changes.put("locale", localeAsInput); + loanProductRelatedDetail.setInterestCalculationPeriodMethod(InterestCalculationPeriodMethod.fromInt(newValue)); + } + + if (command.isChangeInBooleanParameterNamed(LoanProductConstants.ALLOW_PARTIAL_PERIOD_INTEREST_CALCUALTION_PARAM_NAME, + loanProductRelatedDetail.isAllowPartialPeriodInterestCalcualtion())) { + final boolean newValue = command + .booleanPrimitiveValueOfParameterNamed(LoanProductConstants.ALLOW_PARTIAL_PERIOD_INTEREST_CALCUALTION_PARAM_NAME); + changes.put(LoanProductConstants.ALLOW_PARTIAL_PERIOD_INTEREST_CALCUALTION_PARAM_NAME, newValue); + loanProductRelatedDetail.setAllowPartialPeriodInterestCalcualtion(newValue); + } + + if (loanProductRelatedDetail.getInterestCalculationPeriodMethod().isDaily()) { + loanProductRelatedDetail.setAllowPartialPeriodInterestCalcualtion(false); + } + + final String graceOnPrincipalPaymentParamName = "graceOnPrincipalPayment"; + if (command.isChangeInIntegerParameterNamed(graceOnPrincipalPaymentParamName, + loanProductRelatedDetail.getGraceOnPrincipalPayment())) { + final Integer newValue = command.integerValueOfParameterNamed(graceOnPrincipalPaymentParamName); + changes.put(graceOnPrincipalPaymentParamName, newValue); + changes.put("locale", localeAsInput); + loanProductRelatedDetail.setGraceOnPrincipalPayment(newValue); + } + + final String recurringMoratoriumOnPrincipalPeriodsParamName = "recurringMoratoriumOnPrincipalPeriods"; + if (command.isChangeInIntegerParameterNamed(recurringMoratoriumOnPrincipalPeriodsParamName, + loanProductRelatedDetail.getRecurringMoratoriumOnPrincipalPeriods())) { + final Integer newValue = command.integerValueOfParameterNamed(recurringMoratoriumOnPrincipalPeriodsParamName); + changes.put(recurringMoratoriumOnPrincipalPeriodsParamName, newValue); + changes.put("locale", localeAsInput); + loanProductRelatedDetail.setRecurringMoratoriumOnPrincipalPeriods(newValue); + } + + final String graceOnInterestPaymentParamName = "graceOnInterestPayment"; + if (command.isChangeInIntegerParameterNamed(graceOnInterestPaymentParamName, + loanProductRelatedDetail.getGraceOnInterestPayment())) { + final Integer newValue = command.integerValueOfParameterNamed(graceOnInterestPaymentParamName); + changes.put(graceOnInterestPaymentParamName, newValue); + changes.put("locale", localeAsInput); + loanProductRelatedDetail.setGraceOnInterestPayment(newValue); + } + + final String graceOnInterestChargedParamName = "graceOnInterestCharged"; + if (command.isChangeInIntegerParameterNamed(graceOnInterestChargedParamName, + loanProductRelatedDetail.getGraceOnInterestCharged())) { + final Integer newValue = command.integerValueOfParameterNamed(graceOnInterestChargedParamName); + changes.put(graceOnInterestChargedParamName, newValue); + changes.put("locale", localeAsInput); + loanProductRelatedDetail.setGraceOnInterestCharged(newValue); + } + + if (command.isChangeInIntegerParameterNamed(LoanProductConstants.GRACE_ON_ARREARS_AGEING_PARAMETER_NAME, + loanProductRelatedDetail.getGraceOnArrearsAgeing())) { + final Integer newValue = command.integerValueOfParameterNamed(LoanProductConstants.GRACE_ON_ARREARS_AGEING_PARAMETER_NAME); + changes.put(LoanProductConstants.GRACE_ON_ARREARS_AGEING_PARAMETER_NAME, newValue); + changes.put("locale", localeAsInput); + loanProductRelatedDetail.setGraceOnArrearsAgeing(newValue); + } + + if (command.isChangeInIntegerParameterNamed(LoanProductConstants.DAYS_IN_MONTH_TYPE_PARAMETER_NAME, + loanProductRelatedDetail.fetchDaysInMonthType().getValue())) { + final Integer newValue = command.integerValueOfParameterNamed(LoanProductConstants.DAYS_IN_MONTH_TYPE_PARAMETER_NAME); + changes.put(LoanProductConstants.DAYS_IN_MONTH_TYPE_PARAMETER_NAME, newValue); + changes.put("locale", localeAsInput); + loanProductRelatedDetail.setDaysInMonthType(newValue); + } + + if (command.isChangeInIntegerParameterNamed(LoanProductConstants.DAYS_IN_YEAR_TYPE_PARAMETER_NAME, + loanProductRelatedDetail.fetchDaysInYearType().getValue())) { + final Integer newValue = command.integerValueOfParameterNamed(LoanProductConstants.DAYS_IN_YEAR_TYPE_PARAMETER_NAME); + changes.put(LoanProductConstants.DAYS_IN_YEAR_TYPE_PARAMETER_NAME, newValue); + changes.put("locale", localeAsInput); + loanProductRelatedDetail.setDaysInYearType(newValue); + } + + if (command.isChangeInBooleanParameterNamed(LoanProductConstants.IS_INTEREST_RECALCULATION_ENABLED_PARAMETER_NAME, + loanProductRelatedDetail.isInterestRecalculationEnabled())) { + final boolean newValue = command + .booleanPrimitiveValueOfParameterNamed(LoanProductConstants.IS_INTEREST_RECALCULATION_ENABLED_PARAMETER_NAME); + changes.put(LoanProductConstants.IS_INTEREST_RECALCULATION_ENABLED_PARAMETER_NAME, newValue); + loanProductRelatedDetail.setInterestRecalculationEnabled(newValue); + } + + if (command.isChangeInBooleanParameterNamed(LoanProductConstants.IS_EQUAL_AMORTIZATION_PARAM, + loanProductRelatedDetail.isEqualAmortization())) { + final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.IS_EQUAL_AMORTIZATION_PARAM); + changes.put(LoanProductConstants.IS_EQUAL_AMORTIZATION_PARAM, newValue); + loanProductRelatedDetail.setEqualAmortization(newValue); + } + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformServiceImpl.java index 43044ac18..dce06fdf9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleCalculationPlatformServiceImpl.java @@ -24,80 +24,46 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; -import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; +import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.api.JsonQuery; -import org.apache.fineract.infrastructure.core.data.ApiParameterError; -import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; -import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; -import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.organisation.monetary.service.CurrencyReadPlatformService; -import org.apache.fineract.portfolio.accountdetails.domain.AccountType; import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails; +import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; +import org.apache.fineract.portfolio.loanaccount.domain.LoanSummaryWrapper; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; -import org.apache.fineract.portfolio.loanaccount.serialization.CalculateLoanScheduleQueryFromApiJsonHelper; -import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationCommandFromApiJsonHelper; -import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; -import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationValidator; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanScheduleValidator; import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService; -import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; -import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository; -import org.apache.fineract.portfolio.loanproduct.exception.LoanProductNotFoundException; -import org.apache.fineract.portfolio.loanproduct.serialization.LoanProductDataValidator; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service @Transactional +@RequiredArgsConstructor public class LoanScheduleCalculationPlatformServiceImpl implements LoanScheduleCalculationPlatformService { - private final CalculateLoanScheduleQueryFromApiJsonHelper fromApiJsonDeserializer; + private final LoanScheduleValidator fromApiJsonDeserializer; private final LoanScheduleAssembler loanScheduleAssembler; - private final FromJsonHelper fromJsonHelper; - private final LoanProductRepository loanProductRepository; - private final LoanProductDataValidator loanProductCommandFromApiJsonDeserializer; - private final LoanReadPlatformService loanReadPlatformService; - private final LoanApplicationCommandFromApiJsonHelper loanApiJsonDeserializer; - private final LoanAssembler loanAssembler; + private final LoanApplicationValidator loanApiJsonDeserializer; private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory; - private final ConfigurationDomainService configurationDomainService; private final CurrencyReadPlatformService currencyReadPlatformService; private final LoanUtilService loanUtilService; - - @Autowired - public LoanScheduleCalculationPlatformServiceImpl(final CalculateLoanScheduleQueryFromApiJsonHelper fromApiJsonDeserializer, - final LoanScheduleAssembler loanScheduleAssembler, final FromJsonHelper fromJsonHelper, - final LoanProductRepository loanProductRepository, final LoanProductDataValidator loanProductCommandFromApiJsonDeserializer, - final LoanReadPlatformService loanReadPlatformService, final LoanApplicationCommandFromApiJsonHelper loanApiJsonDeserializer, - final LoanAssembler loanAssembler, - final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory, - final ConfigurationDomainService configurationDomainService, final CurrencyReadPlatformService currencyReadPlatformService, - final LoanUtilService loanUtilService) { - this.fromApiJsonDeserializer = fromApiJsonDeserializer; - this.loanScheduleAssembler = loanScheduleAssembler; - this.fromJsonHelper = fromJsonHelper; - this.loanProductRepository = loanProductRepository; - this.loanProductCommandFromApiJsonDeserializer = loanProductCommandFromApiJsonDeserializer; - this.loanReadPlatformService = loanReadPlatformService; - this.loanApiJsonDeserializer = loanApiJsonDeserializer; - this.loanAssembler = loanAssembler; - this.loanRepaymentScheduleTransactionProcessorFactory = loanRepaymentScheduleTransactionProcessorFactory; - this.configurationDomainService = configurationDomainService; - this.currencyReadPlatformService = currencyReadPlatformService; - this.loanUtilService = loanUtilService; - } + private final LoanRepositoryWrapper loanRepository; + private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine; + private final LoanSummaryWrapper loanSummaryWrapper; @Override public LoanScheduleModel calculateLoanSchedule(final JsonQuery query, Boolean validateParams) { @@ -105,44 +71,19 @@ public class LoanScheduleCalculationPlatformServiceImpl implements LoanScheduleC /*** * TODO: Vishwas, this is probably not required, test and remove the same **/ - final Long productId = this.fromJsonHelper.extractLongNamed("productId", query.parsedJson()); - final LoanProduct loanProduct = this.loanProductRepository.findById(productId) - .orElseThrow(() -> new LoanProductNotFoundException(productId)); if (validateParams) { - boolean isMeetingMandatoryForJLGLoans = configurationDomainService.isMeetingMandatoryForJLGLoans(); - this.loanApiJsonDeserializer.validateForCreate(query.json(), isMeetingMandatoryForJLGLoans, loanProduct); + this.loanApiJsonDeserializer.validateForCreate(query); } this.fromApiJsonDeserializer.validate(query.json()); - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan"); - - if (loanProduct.isUseBorrowerCycle()) { - final Long clientId = this.fromJsonHelper.extractLongNamed("clientId", query.parsedJson()); - final Long groupId = this.fromJsonHelper.extractLongNamed("groupId", query.parsedJson()); - Integer cycleNumber = 0; - if (clientId != null) { - cycleNumber = this.loanReadPlatformService.retriveLoanCounter(clientId, loanProduct.getId()); - } else if (groupId != null) { - cycleNumber = this.loanReadPlatformService.retriveLoanCounter(groupId, AccountType.GROUP.getValue(), loanProduct.getId()); - } - this.loanProductCommandFromApiJsonDeserializer.validateMinMaxConstraints(query.parsedJson(), baseDataValidator, loanProduct, - cycleNumber); - } else { - this.loanProductCommandFromApiJsonDeserializer.validateMinMaxConstraints(query.parsedJson(), baseDataValidator, loanProduct); - } - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - return this.loanScheduleAssembler.assembleLoanScheduleFrom(query.parsedJson()); } @Override public void updateFutureSchedule(LoanScheduleData loanScheduleData, final Long loanId) { - final Loan loan = this.loanAssembler.assembleFrom(loanId); + final Loan loan = fetchLoan(loanId); LocalDate today = DateUtils.getBusinessLocalDate(); final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory @@ -210,7 +151,7 @@ public class LoanScheduleCalculationPlatformServiceImpl implements LoanScheduleC @Override @Transactional(readOnly = true) public LoanScheduleData generateLoanScheduleForVariableInstallmentRequest(Long loanId, final String json) { - final Loan loan = this.loanAssembler.assembleFrom(loanId); + final Loan loan = fetchLoan(loanId); this.loanScheduleAssembler.assempleVariableScheduleFrom(loan, json); return constructLoanScheduleData(loan); } @@ -262,19 +203,23 @@ public class LoanScheduleCalculationPlatformServiceImpl implements LoanScheduleC CurrencyData currencyData = this.currencyReadPlatformService.retrieveCurrency(currency.getCode()); - LoanScheduleData scheduleData = new LoanScheduleData(currencyData, installmentData, - loan.getLoanRepaymentScheduleDetail().getNumberOfRepayments(), principal.getAmount(), principal.getAmount(), - totalInterest.getAmount(), totalCharge.getAmount(), totalPenalty.getAmount(), + return new LoanScheduleData(currencyData, installmentData, loan.getLoanRepaymentScheduleDetail().getNumberOfRepayments(), + principal.getAmount(), principal.getAmount(), totalInterest.getAmount(), totalCharge.getAmount(), totalPenalty.getAmount(), principal.plus(totalCharge).plus(totalInterest).plus(totalPenalty).getAmount()); - - return scheduleData; } private LoanApplicationTerms constructLoanApplicationTerms(final Loan loan) { final LocalDate recalculateFrom = null; ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); - LoanApplicationTerms loanApplicationTerms = loan.constructLoanApplicationTerms(scheduleGeneratorDTO); - return loanApplicationTerms; + return loan.constructLoanApplicationTerms(scheduleGeneratorDTO); + } + + private Loan fetchLoan(final Long accountId) { + final Loan loanAccount = this.loanRepository.findOneWithNotFoundDetection(accountId, true); + loanAccount.setHelpers(defaultLoanLifecycleStateMachine, this.loanSummaryWrapper, + this.loanRepaymentScheduleTransactionProcessorFactory); + + return loanAccount; } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java similarity index 67% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java rename to fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java index f11acacd7..6cb8c9222 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java @@ -27,6 +27,7 @@ import java.math.BigDecimal; import java.time.LocalDate; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Locale; @@ -35,39 +36,90 @@ import java.util.Set; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; +import org.apache.fineract.infrastructure.configuration.domain.GlobalConfigurationProperty; +import org.apache.fineract.infrastructure.configuration.domain.GlobalConfigurationRepositoryWrapper; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.api.JsonQuery; import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; +import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; import org.apache.fineract.infrastructure.core.exception.InvalidJsonException; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.exception.UnsupportedParameterException; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; +import org.apache.fineract.infrastructure.entityaccess.FineractEntityAccessConstants; +import org.apache.fineract.infrastructure.entityaccess.domain.FineractEntityAccessType; +import org.apache.fineract.infrastructure.entityaccess.domain.FineractEntityRelation; +import org.apache.fineract.infrastructure.entityaccess.domain.FineractEntityRelationRepository; +import org.apache.fineract.infrastructure.entityaccess.domain.FineractEntityToEntityMapping; +import org.apache.fineract.infrastructure.entityaccess.domain.FineractEntityToEntityMappingRepository; +import org.apache.fineract.infrastructure.entityaccess.exception.NotOfficeSpecificProductException; +import org.apache.fineract.organisation.holiday.domain.Holiday; +import org.apache.fineract.organisation.holiday.domain.HolidayRepository; +import org.apache.fineract.organisation.holiday.domain.HolidayStatusType; +import org.apache.fineract.organisation.holiday.service.HolidayUtil; +import org.apache.fineract.organisation.monetary.domain.MoneyHelper; +import org.apache.fineract.organisation.workingdays.domain.WorkingDays; +import org.apache.fineract.organisation.workingdays.domain.WorkingDaysRepositoryWrapper; +import org.apache.fineract.organisation.workingdays.service.WorkingDaysUtil; import org.apache.fineract.portfolio.accountdetails.domain.AccountType; import org.apache.fineract.portfolio.calendar.service.CalendarUtils; +import org.apache.fineract.portfolio.client.domain.Client; +import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper; +import org.apache.fineract.portfolio.client.exception.ClientNotActiveException; import org.apache.fineract.portfolio.collateralmanagement.domain.ClientCollateralManagement; import org.apache.fineract.portfolio.collateralmanagement.domain.ClientCollateralManagementRepositoryWrapper; +import org.apache.fineract.portfolio.collateralmanagement.domain.CollateralManagementDomain; +import org.apache.fineract.portfolio.collateralmanagement.service.LoanCollateralAssembler; +import org.apache.fineract.portfolio.group.domain.Group; +import org.apache.fineract.portfolio.group.domain.GroupRepositoryWrapper; +import org.apache.fineract.portfolio.group.exception.ClientNotInGroupException; +import org.apache.fineract.portfolio.group.exception.GroupNotActiveException; import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagement; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; +import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.exception.ExceedingTrancheCountException; import org.apache.fineract.portfolio.loanaccount.exception.InvalidAmountOfCollateralQuantity; import org.apache.fineract.portfolio.loanaccount.exception.InvalidAmountOfCollaterals; +import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanStateTransitionException; +import org.apache.fineract.portfolio.loanaccount.exception.LoanApplicationDateException; +import org.apache.fineract.portfolio.loanaccount.exception.LoanApplicationNotInSubmittedAndPendingApprovalStateCannotBeModified; +import org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementDataNotAllowedException; +import org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementDataRequiredException; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; +import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; import org.apache.fineract.portfolio.loanproduct.LoanProductConstants; +import org.apache.fineract.portfolio.loanproduct.data.LoanProductData; import org.apache.fineract.portfolio.loanproduct.domain.AdvancedPaymentAllocationsValidator; import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod; import org.apache.fineract.portfolio.loanproduct.domain.InterestCalculationPeriodMethod; import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod; import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductPaymentAllocationRule; +import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository; import org.apache.fineract.portfolio.loanproduct.exception.EqualAmortizationUnsupportedFeatureException; +import org.apache.fineract.portfolio.loanproduct.exception.LoanProductNotFoundException; +import org.apache.fineract.portfolio.loanproduct.serialization.LoanProductDataValidator; +import org.apache.fineract.portfolio.loanproduct.service.LoanProductReadPlatformService; import org.apache.fineract.portfolio.savings.domain.SavingsAccount; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper; import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; @Slf4j -@RequiredArgsConstructor @Component -public final class LoanApplicationCommandFromApiJsonHelper { +@RequiredArgsConstructor +public final class LoanApplicationValidator { /** * The parameters supported for this command. @@ -96,7 +148,7 @@ public final class LoanApplicationCommandFromApiJsonHelper { LoanApiConstants.calendarIdParameterName, // optional LoanApiConstants.syncDisbursementWithMeetingParameterName, // optional LoanApiConstants.linkAccountIdParameterName, LoanApiConstants.disbursementDataParameterName, - LoanApiConstants.emiAmountParameterName, LoanApiConstants.maxOutstandingBalanceParameterName, + LoanApiConstants.fixedEmiAmountParameterName, LoanApiConstants.maxOutstandingBalanceParameterName, LoanProductConstants.GRACE_ON_ARREARS_AGEING_PARAMETER_NAME, LoanApiConstants.createStandingInstructionAtDisbursementParameterName, LoanApiConstants.isTopup, LoanApiConstants.loanIdToClose, LoanApiConstants.datatables, LoanApiConstants.isEqualAmortizationParam, LoanProductConstants.RATES_PARAM_NAME, @@ -109,27 +161,93 @@ public final class LoanApplicationCommandFromApiJsonHelper { public static final String LOANAPPLICATION_UNDO = "loanapplication.undo"; private final FromJsonHelper fromApiJsonHelper; - private final CalculateLoanScheduleQueryFromApiJsonHelper apiJsonHelper; + private final LoanScheduleValidator loanScheduleValidator; private final ClientCollateralManagementRepositoryWrapper clientCollateralManagementRepositoryWrapper; private final LoanChargeApiJsonValidator loanChargeApiJsonValidator; private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory; private final AdvancedPaymentAllocationsValidator advancedPaymentAllocationsValidator; + private final ConfigurationDomainService configurationDomainService; + private final LoanProductRepository loanProductRepository; + private final ClientRepositoryWrapper clientRepository; + private final GroupRepositoryWrapper groupRepository; + private final LoanReadPlatformService loanReadPlatformService; + private final LoanProductDataValidator loanProductDataValidator; + private final GlobalConfigurationRepositoryWrapper globalConfigurationRepository; + private final FineractEntityToEntityMappingRepository entityMappingRepository; + private final FineractEntityRelationRepository fineractEntityRelationRepository; + private final LoanRepositoryWrapper loanRepositoryWrapper; + private final LoanProductReadPlatformService loanProductReadPlatformService; + private final LoanCollateralAssembler collateralAssembler; + private final WorkingDaysRepositoryWrapper workingDaysRepository; + private final HolidayRepository holidayRepository; + private final SavingsAccountRepositoryWrapper savingsAccountRepository; - public void validateForCreate(final String json, final boolean isMeetingMandatoryForJLGLoans, final LoanProduct loanProduct) { - if (StringUtils.isBlank(json)) { - throw new InvalidJsonException(); + public void validateForCreate(final Loan loan) { + final LocalDate expectedFirstRepaymentOnDate = loan.getExpectedFirstRepaymentOnDate(); + final LocalDate submittedOnDate = loan.getSubmittedOnDate(); + if (expectedFirstRepaymentOnDate != null && DateUtils.isAfter(submittedOnDate, expectedFirstRepaymentOnDate)) { + throw new LoanApplicationDateException("submitted.on.date.cannot.be.after.the.loan.expected.first.repayment.date", + "submittedOnDate cannot be after the loans expectedFirstRepaymentOnDate.", submittedOnDate, + expectedFirstRepaymentOnDate); } - final Type typeOfMap = new TypeToken>() { + validateLoanTermAndRepaidEveryValues(loan.getTermFrequency(), loan.getTermPeriodFrequencyType(), + loan.repaymentScheduleDetail().getNumberOfRepayments(), loan.repaymentScheduleDetail().getRepayEvery(), + loan.repaymentScheduleDetail().getRepaymentPeriodFrequencyType().getValue(), loan); + } - }.getType(); - this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, SUPPORTED_PARAMETERS); + public void validateForModify(final Loan loan) { + final LocalDate expectedFirstRepaymentOnDate = loan.getExpectedFirstRepaymentOnDate(); + final LocalDate submittedOnDate = loan.getSubmittedOnDate(); + if (expectedFirstRepaymentOnDate != null && DateUtils.isAfter(submittedOnDate, expectedFirstRepaymentOnDate)) { + throw new LoanApplicationDateException("submitted.on.date.cannot.be.after.the.loan.expected.first.repayment.date", + "submittedOnDate cannot be after the loans expectedFirstRepaymentOnDate.", submittedOnDate, + expectedFirstRepaymentOnDate); + } + + validateLoanTermAndRepaidEveryValues(loan.getTermFrequency(), loan.getTermPeriodFrequencyType(), + loan.repaymentScheduleDetail().getNumberOfRepayments(), loan.repaymentScheduleDetail().getRepayEvery(), + loan.repaymentScheduleDetail().getRepaymentPeriodFrequencyType().getValue(), loan); + } + + public void validateForCreate(JsonCommand command) { + String json = command.json(); + validateRequestBody(json); + validateForSupportedParameters(json); + final JsonElement element = this.fromApiJsonHelper.parse(json); + validateForCreate(element); + } + + public void validateForCreate(JsonQuery query) { + String json = query.json(); + validateRequestBody(json); + final JsonElement element = this.fromApiJsonHelper.parse(json); + validateForSupportedParameters(json); + validateForCreate(element); + } + + private void validateForCreate(final JsonElement element) { + boolean isMeetingMandatoryForJLGLoans = configurationDomainService.isMeetingMandatoryForJLGLoans(); + + final Long productId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.productIdParameterName, element); + final LoanProduct loanProduct = this.loanProductRepository.findById(productId) + .orElseThrow(() -> new LoanProductNotFoundException(productId)); + + 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); + } + validateClientOrGroup(client, group, productId); final List dataValidationErrors = new ArrayList<>(); final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan"); - final JsonElement element = this.fromApiJsonHelper.parse(json); - final String loanTypeStr = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.loanTypeParameterName, element); baseDataValidator.reset().parameter(LoanApiConstants.loanTypeParameterName).value(loanTypeStr).notNull(); @@ -137,8 +255,6 @@ public final class LoanApplicationCommandFromApiJsonHelper { final AccountType loanType = AccountType.fromName(loanTypeStr); baseDataValidator.reset().parameter(LoanApiConstants.loanTypeParameterName).value(loanType.getValue()).inMinMaxRange(1, 4); - final Long clientId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.clientIdParameterName, element); - final Long groupId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.groupIdParameterName, element); if (loanType.isIndividualAccount()) { baseDataValidator.reset().parameter(LoanApiConstants.clientIdParameterName).value(clientId).notNull().longGreaterThanZero(); baseDataValidator.reset().parameter(LoanApiConstants.groupIdParameterName).value(groupId) @@ -193,7 +309,6 @@ public final class LoanApplicationCommandFromApiJsonHelper { baseDataValidator.reset().parameter(LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName) .value(fixedPrincipalPercentagePerInstallment).notLessThanMin(BigDecimal.ONE).notGreaterThanMax(BigDecimal.valueOf(100)); - final Long productId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.productIdParameterName, element); baseDataValidator.reset().parameter(LoanApiConstants.productIdParameterName).value(productId).notNull().integerGreaterThanZero(); if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.accountNoParameterName, element)) { @@ -265,6 +380,7 @@ public final class LoanApplicationCommandFromApiJsonHelper { baseDataValidator.reset().parameter(LoanApiConstants.interestCalculationPeriodTypeParameterName) .value(interestCalculationPeriodType).notNull().inMinMaxRange(0, 1); + boolean isInterestBearing = false; if (loanProduct.isLinkedToFloatingInterestRate()) { if (isEqualAmortization) { throw new EqualAmortizationUnsupportedFeatureException("floating.interest.rate", "floating interest rate"); @@ -300,7 +416,7 @@ public final class LoanApplicationCommandFromApiJsonHelper { 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)) { @@ -318,7 +434,7 @@ public final class LoanApplicationCommandFromApiJsonHelper { .extractBigDecimalWithLocaleNamed(LoanApiConstants.interestRatePerPeriodParameterName, element); baseDataValidator.reset().parameter(LoanApiConstants.interestRatePerPeriodParameterName).value(interestRatePerPeriod).notNull() .zeroOrPositiveAmount(); - + isInterestBearing = interestRatePerPeriod.compareTo(BigDecimal.ZERO) > 0; } final Integer amortizationType = this.fromApiJsonHelper @@ -381,9 +497,8 @@ public final class LoanApplicationCommandFromApiJsonHelper { final LocalDate submittedOnDate = this.fromApiJsonHelper.extractLocalDateNamed(LoanApiConstants.submittedOnDateParameterName, element); - if (submittedOnDate == null) { - baseDataValidator.reset().parameter(LoanApiConstants.submittedOnDateParameterName).value(submittedOnDate).notNull(); - } + + baseDataValidator.reset().parameter(LoanApiConstants.submittedOnDateParameterName).value(submittedOnDate).notNull(); if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.submittedOnNoteParameterName, element)) { final String submittedOnNote = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.submittedOnNoteParameterName, @@ -394,24 +509,10 @@ public final class LoanApplicationCommandFromApiJsonHelper { final String transactionProcessingStrategy = this.fromApiJsonHelper .extractStringNamed(LoanApiConstants.transactionProcessingStrategyCodeParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.transactionProcessingStrategyCodeParameterName) - .value(transactionProcessingStrategy).notNull(); - 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"); - } - // Validating whether the processor is existing - loanRepaymentScheduleTransactionProcessorFactory.determineProcessor(transactionProcessingStrategy); + validateTransactionProcessingStrategy(transactionProcessingStrategy, loanProduct, baseDataValidator); - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.linkAccountIdParameterName, element)) { - final Long linkAccountId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.linkAccountIdParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.linkAccountIdParameterName).value(linkAccountId).ignoreIfNull() - .longGreaterThanZero(); - } + validateLinkedSavingsAccount(element, baseDataValidator); if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.createStandingInstructionAtDisbursementParameterName, element)) { final Boolean createStandingInstructionAtDisbursement = this.fromApiJsonHelper @@ -425,43 +526,7 @@ public final class LoanApplicationCommandFromApiJsonHelper { } // charges - if (element.isJsonObject() && this.fromApiJsonHelper.parameterExists(LoanApiConstants.chargesParameterName, element)) { - final JsonObject topLevelJsonElement = element.getAsJsonObject(); - final String dateFormat = this.fromApiJsonHelper.extractDateFormatParameter(topLevelJsonElement); - final Locale locale = this.fromApiJsonHelper.extractLocaleParameter(topLevelJsonElement); - - if (topLevelJsonElement.get(LoanApiConstants.chargesParameterName).isJsonArray()) { - final Type arrayObjectParameterTypeOfMap = new TypeToken>() { - - }.getType(); - final Set supportedParameters = new HashSet<>( - Arrays.asList(LoanApiConstants.idParameterName, LoanApiConstants.chargeIdParameterName, - LoanApiConstants.amountParameterName, LoanApiConstants.chargeTimeTypeParameterName, - LoanApiConstants.chargeCalculationTypeParameterName, LoanApiConstants.dueDateParamName)); - - final JsonArray array = topLevelJsonElement.get(LoanApiConstants.chargesParameterName).getAsJsonArray(); - for (int i = 1; i <= array.size(); i++) { - - final JsonObject loanChargeElement = array.get(i - 1).getAsJsonObject(); - final String arrayObjectJson = this.fromApiJsonHelper.toJson(loanChargeElement); - this.fromApiJsonHelper.checkForUnsupportedParameters(arrayObjectParameterTypeOfMap, arrayObjectJson, - supportedParameters); - - final Long chargeId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.chargeIdParameterName, - loanChargeElement); - baseDataValidator.reset().parameter(LoanApiConstants.chargesParameterName) - .parameterAtIndexArray(LoanApiConstants.chargeIdParameterName, i).value(chargeId).notNull() - .integerGreaterThanZero(); - - final BigDecimal amount = this.fromApiJsonHelper.extractBigDecimalNamed(LoanApiConstants.amountParameterName, - loanChargeElement, locale); - baseDataValidator.reset().parameter(LoanApiConstants.chargesParameterName) - .parameterAtIndexArray(LoanApiConstants.amountParameterName, i).value(amount).notNull().positiveAmount(); - - this.fromApiJsonHelper.extractLocalDateNamed(LoanApiConstants.dueDateParamName, loanChargeElement, dateFormat, locale); - } - } - } + loanChargeApiJsonValidator.validateLoanCharges(element, loanProduct, baseDataValidator); /** * TODO: Add collaterals for other loan accounts if needed. For now it's only applicable for individual @@ -509,6 +574,7 @@ public final class LoanApplicationCommandFromApiJsonHelper { if (clientCollateralId != null && BigDecimal.valueOf(0).compareTo(clientCollateralManagement.getQuantity()) >= 0) { throw new InvalidAmountOfCollateralQuantity(clientCollateralManagement.getQuantity()); } + } } else { baseDataValidator.reset().parameter(LoanApiConstants.collateralParameterName).expectedArrayButIsNot(); @@ -516,18 +582,19 @@ public final class LoanApplicationCommandFromApiJsonHelper { } } - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.emiAmountParameterName, element)) { + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.fixedEmiAmountParameterName, element)) { if (!(loanProduct.isCanDefineInstallmentAmount() || loanProduct.isMultiDisburseLoan())) { List unsupportedParameterList = new ArrayList<>(); - unsupportedParameterList.add(LoanApiConstants.emiAmountParameterName); + unsupportedParameterList.add(LoanApiConstants.fixedEmiAmountParameterName); throw new UnsupportedParameterException(unsupportedParameterList); } if (isEqualAmortization) { throw new EqualAmortizationUnsupportedFeatureException("fixed.emi", "fixed emi"); } - final BigDecimal emiAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.emiAmountParameterName, - element); - baseDataValidator.reset().parameter(LoanApiConstants.emiAmountParameterName).value(emiAmount).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 @@ -543,6 +610,47 @@ public final class LoanApplicationCommandFromApiJsonHelper { 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)) { @@ -598,41 +706,149 @@ public final class LoanApplicationCommandFromApiJsonHelper { } } + validateBorrowerCycle(element, loanProduct, clientId, groupId, baseDataValidator); + + loanProductDataValidator.fixedLengthValidations(transactionProcessingStrategy, isInterestBearing, numberOfRepayments, + repaymentEvery, element, baseDataValidator); + + // Validate If the externalId is already registered + final String externalIdStr = this.fromApiJsonHelper.extractStringNamed("externalId", element); + ExternalId externalId = ExternalIdFactory.produce(externalIdStr); + if (!externalId.isEmpty()) { + final boolean existByExternalId = this.loanRepositoryWrapper.existLoanByExternalId(externalId); + if (existByExternalId) { + throw new GeneralPlatformDomainRuleException("error.msg.loan.with.externalId.already.used", + "Loan with externalId is already registered."); + } + } + + checkForProductMixRestrictions(element); + validateSubmittedOnDate(element, null, loanProduct); + validateDisbursementDetails(loanProduct, element); + validateCollateral(element); + // validate if disbursement date is a holiday or a non-working day + validateDisbursementDateIsOnNonWorkingDay(expectedDisbursementDate); + Long officeId = client != null ? client.getOffice().getId() : group.getOffice().getId(); + validateDisbursementDateIsOnHoliday(expectedDisbursementDate, officeId); + final Integer recurringMoratoriumOnPrincipalPeriods = this.fromApiJsonHelper + .extractIntegerWithLocaleNamed("recurringMoratoriumOnPrincipalPeriods", element); + loanProductDataValidator.validateRepaymentPeriodWithGraceSettings(numberOfRepayments, graceOnPrincipalPayment, + graceOnInterestPayment, graceOnInterestCharged, recurringMoratoriumOnPrincipalPeriods, baseDataValidator); + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } - public void validateForModify(final String json, final LoanProduct loanProduct, final Loan existingLoanApplication) { - if (StringUtils.isBlank(json)) { - throw new InvalidJsonException(); + private void validateBorrowerCycle(JsonElement element, LoanProduct loanProduct, Long clientId, Long groupId, + DataValidatorBuilder baseDataValidator) { + if (loanProduct.isUseBorrowerCycle()) { + Integer cycleNumber = 0; + if (clientId != null) { + cycleNumber = this.loanReadPlatformService.retriveLoanCounter(clientId, loanProduct.getId()); + } else if (groupId != null) { + cycleNumber = this.loanReadPlatformService.retriveLoanCounter(groupId, AccountType.GROUP.getValue(), loanProduct.getId()); + } + this.loanProductDataValidator.validateMinMaxConstraints(element, baseDataValidator, loanProduct, cycleNumber); + } else { + this.loanProductDataValidator.validateMinMaxConstraints(element, baseDataValidator, loanProduct); + } + } + + private void validateDisbursementDateIsOnNonWorkingDay(final LocalDate expectedDisbursementDate) { + final WorkingDays workingDays = this.workingDaysRepository.findOne(); + final boolean allowTransactionsOnNonWorkingDay = this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled(); + if (!allowTransactionsOnNonWorkingDay && !WorkingDaysUtil.isWorkingDay(workingDays, expectedDisbursementDate)) { + final String errorMessage = "Expected disbursement date cannot be on a non working day"; + throw new LoanApplicationDateException("disbursement.date.on.non.working.day", errorMessage, expectedDisbursementDate); + } + } + + private void validateDisbursementDateIsOnHoliday(final LocalDate expectedDisbursementDate, final Long officeId) { + final List holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(officeId, expectedDisbursementDate, + HolidayStatusType.ACTIVE.getValue()); + + final boolean allowTransactionsOnHoliday = this.configurationDomainService.allowTransactionsOnHolidayEnabled(); + if (!allowTransactionsOnHoliday && HolidayUtil.isHoliday(expectedDisbursementDate, holidays)) { + final String errorMessage = "Expected disbursement date cannot be on a holiday"; + throw new LoanApplicationDateException("disbursement.date.on.holiday", errorMessage, expectedDisbursementDate); + } + } + + private void validateCollateral(JsonElement element) { + final BigDecimal amount = this.fromApiJsonHelper + .extractBigDecimalWithLocaleNamed(LoanApiConstants.disbursementPrincipalParameterName, element); + final String loanTypeStr = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.loanTypeParameterName, element); + if (!StringUtils.isBlank(loanTypeStr)) { + final AccountType loanAccountType = AccountType.fromName(loanTypeStr); + if (loanAccountType.isIndividualAccount()) { + Set collateral = this.collateralAssembler.fromParsedJson(element); + if (!collateral.isEmpty()) { + BigDecimal totalValue = BigDecimal.ZERO; + for (LoanCollateralManagement collateralManagement : collateral) { + final CollateralManagementDomain collateralManagementDomain = collateralManagement.getClientCollateralManagement() + .getCollaterals(); + BigDecimal totalCollateral = collateralManagement.getQuantity().multiply(collateralManagementDomain.getBasePrice()) + .multiply(collateralManagementDomain.getPctToBase()) + .divide(BigDecimal.valueOf(100), MoneyHelper.getMathContext()); + totalValue = totalValue.add(totalCollateral); + } + if (amount.compareTo(totalValue) > 0) { + throw new InvalidAmountOfCollaterals(totalValue); + } + } + } + } + } + + public void validateForModify(final JsonCommand command, final Loan loan) { + String json = command.json(); + validateRequestBody(json); + + validateForSupportedParameters(json); + + if (!loan.isSubmittedAndPendingApproval()) { + throw new LoanApplicationNotInSubmittedAndPendingApprovalStateCannotBeModified(loan.getId()); } - final Type typeOfMap = new TypeToken>() { - - }.getType(); - this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, SUPPORTED_PARAMETERS); + // If new loan product to be set + LoanProduct loanProduct; + final String productIdParamName = "productId"; + final Long productId = command.longValueOfParameterNamed(productIdParamName); + if (productId == null || productId.equals(loan.getLoanProduct().getId())) { + loanProduct = loan.getLoanProduct(); + } else { + 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; - final Long clientId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.clientIdParameterName, element); + 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; - final Long groupId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.groupIdParameterName, element); + 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 (this.fromApiJsonHelper.parameterExists(LoanApiConstants.productIdParameterName, element)) { + if (productId != null) { atLeastOneParameterPassedForUpdate = true; - final Long productId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.productIdParameterName, element); baseDataValidator.reset().parameter(LoanApiConstants.productIdParameterName).value(productId).notNull() .integerGreaterThanZero(); } @@ -644,7 +860,7 @@ public final class LoanApplicationCommandFromApiJsonHelper { .notExceedingLengthOf(20); } - boolean isEqualAmortization = existingLoanApplication.getLoanProductRelatedDetail().isEqualAmortization(); + 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() @@ -679,7 +895,7 @@ public final class LoanApplicationCommandFromApiJsonHelper { .integerGreaterThanZero(); } - String transactionProcessingStrategy = existingLoanApplication.getTransactionProcessingStrategyCode(); + String transactionProcessingStrategy = loan.getTransactionProcessingStrategyCode(); if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.transactionProcessingStrategyCodeParameterName, element)) { atLeastOneParameterPassedForUpdate = true; transactionProcessingStrategy = this.fromApiJsonHelper @@ -687,7 +903,7 @@ public final class LoanApplicationCommandFromApiJsonHelper { baseDataValidator.reset().parameter(LoanApiConstants.transactionProcessingStrategyCodeParameterName) .value(transactionProcessingStrategy).notNull(); // Validating whether the processor is existing - loanRepaymentScheduleTransactionProcessorFactory.determineProcessor(transactionProcessingStrategy); + validateTransactionProcessingStrategy(transactionProcessingStrategy, loanProduct, baseDataValidator); } if (!AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY @@ -729,10 +945,11 @@ public final class LoanApplicationCommandFromApiJsonHelper { .inMinMaxRange(0, 3); } + Integer numberOfRepayments = loan.getNumberOfRepayments(); if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.numberOfRepaymentsParameterName, element)) { atLeastOneParameterPassedForUpdate = true; - final Integer numberOfRepayments = this.fromApiJsonHelper - .extractIntegerWithLocaleNamed(LoanApiConstants.numberOfRepaymentsParameterName, element); + numberOfRepayments = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(LoanApiConstants.numberOfRepaymentsParameterName, + element); baseDataValidator.reset().parameter(LoanApiConstants.numberOfRepaymentsParameterName).value(numberOfRepayments).notNull() .integerGreaterThanZero(); } @@ -774,7 +991,7 @@ public final class LoanApplicationCommandFromApiJsonHelper { "interestRatePerPeriod param is not supported, selected Loan Product is linked with floating interest rate."); } - Boolean isFloatingInterestRate = existingLoanApplication.getIsFloatingInterestRate(); + Boolean isFloatingInterestRate = loan.getIsFloatingInterestRate(); if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.isFloatingInterestRate, element)) { isFloatingInterestRate = this.fromApiJsonHelper.extractBooleanNamed(LoanApiConstants.isFloatingInterestRate, element); atLeastOneParameterPassedForUpdate = true; @@ -790,7 +1007,7 @@ public final class LoanApplicationCommandFromApiJsonHelper { } if (interestType == null) { - interestType = existingLoanApplication.getLoanProductRelatedDetail().getInterestMethod().getValue(); + interestType = loan.getLoanProductRelatedDetail().getInterestMethod().getValue(); } if (interestType != null && interestType.equals(InterestMethod.FLAT.getValue())) { baseDataValidator.reset().parameter(LoanApiConstants.interestTypeParameterName).failWithCode( @@ -798,7 +1015,7 @@ public final class LoanApplicationCommandFromApiJsonHelper { "interestType should be DECLINING_BALANCE for selected Loan Product as it is linked to floating rates."); } - BigDecimal interestRateDifferential = existingLoanApplication.getInterestRateDifferential(); + BigDecimal interestRateDifferential = loan.getInterestRateDifferential(); if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.interestRateDifferentialParameterName, element)) { interestRateDifferential = this.fromApiJsonHelper .extractBigDecimalWithLocaleNamed(LoanApiConstants.interestRateDifferentialParameterName, element); @@ -821,7 +1038,7 @@ public final class LoanApplicationCommandFromApiJsonHelper { "interestRateDifferential param is not supported, selected Loan Product is not linked with floating interest rate."); } - BigDecimal interestRatePerPeriod = existingLoanApplication.getLoanProductRelatedDetail().getNominalInterestRatePerPeriod(); + BigDecimal interestRatePerPeriod = loan.getLoanProductRelatedDetail().getNominalInterestRatePerPeriod(); if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.interestRatePerPeriodParameterName, element)) { this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.interestRatePerPeriodParameterName, element); atLeastOneParameterPassedForUpdate = true; @@ -856,7 +1073,7 @@ public final class LoanApplicationCommandFromApiJsonHelper { "Principal fixing cannot be done with equal installment amortization"); } - LocalDate expectedDisbursementDate = null; + LocalDate expectedDisbursementDate = loan.getExpectedDisbursementDate(); if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.expectedDisbursementDateParameterName, element)) { atLeastOneParameterPassedForUpdate = true; @@ -871,23 +1088,26 @@ public final class LoanApplicationCommandFromApiJsonHelper { .notNull(); } + Integer graceOnPrincipalPayment = loan.getLoanProductRelatedDetail().getGraceOnPrincipalPayment(); // grace validation if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.graceOnPrincipalPaymentParameterName, element)) { - final Integer graceOnPrincipalPayment = this.fromApiJsonHelper + graceOnPrincipalPayment = this.fromApiJsonHelper .extractIntegerWithLocaleNamed(LoanApiConstants.graceOnPrincipalPaymentParameterName, element); baseDataValidator.reset().parameter(LoanApiConstants.graceOnPrincipalPaymentParameterName).value(graceOnPrincipalPayment) .zeroOrPositiveAmount(); } + Integer graceOnInterestPayment = loan.getLoanProductRelatedDetail().getGraceOnInterestPayment(); if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.graceOnInterestPaymentParameterName, element)) { - final Integer graceOnInterestPayment = this.fromApiJsonHelper + graceOnInterestPayment = this.fromApiJsonHelper .extractIntegerWithLocaleNamed(LoanApiConstants.graceOnInterestPaymentParameterName, element); baseDataValidator.reset().parameter(LoanApiConstants.graceOnInterestPaymentParameterName).value(graceOnInterestPayment) .zeroOrPositiveAmount(); } + Integer graceOnInterestCharged = loan.getLoanProductRelatedDetail().getGraceOnInterestCharged(); if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.graceOnInterestChargedParameterName, element)) { - final Integer graceOnInterestCharged = this.fromApiJsonHelper + graceOnInterestCharged = this.fromApiJsonHelper .extractIntegerWithLocaleNamed(LoanApiConstants.graceOnInterestChargedParameterName, element); baseDataValidator.reset().parameter(LoanApiConstants.graceOnInterestChargedParameterName).value(graceOnInterestCharged) .zeroOrPositiveAmount(); @@ -914,7 +1134,7 @@ public final class LoanApplicationCommandFromApiJsonHelper { .extractLocalDateNamed(LoanApiConstants.repaymentsStartingFromDateParameterName, element); baseDataValidator.reset().parameter(LoanApiConstants.repaymentsStartingFromDateParameterName).value(repaymentsStartingFromDate) .ignoreIfNull(); - if (!existingLoanApplication.getLoanTermVariations().isEmpty()) { + if (!loan.getLoanTermVariations().isEmpty()) { baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("cannot.modify.application.due.to.variable.installments"); } } @@ -934,53 +1154,10 @@ public final class LoanApplicationCommandFromApiJsonHelper { .notExceedingLengthOf(500); } - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.submittedOnNoteParameterName, element)) { - atLeastOneParameterPassedForUpdate = true; - final Long linkAccountId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.linkAccountIdParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.linkAccountIdParameterName).value(linkAccountId).ignoreIfNull() - .longGreaterThanZero(); - } + validateLinkedSavingsAccount(element, baseDataValidator); // charges - if (element.isJsonObject() && this.fromApiJsonHelper.parameterExists(LoanApiConstants.chargesParameterName, element)) { - atLeastOneParameterPassedForUpdate = true; - - final JsonObject topLevelJsonElement = element.getAsJsonObject(); - final String dateFormat = this.fromApiJsonHelper.extractDateFormatParameter(topLevelJsonElement); - final Locale locale = this.fromApiJsonHelper.extractLocaleParameter(topLevelJsonElement); - - if (topLevelJsonElement.get(LoanApiConstants.chargesParameterName).isJsonArray()) { - final Type arrayObjectParameterTypeOfMap = new TypeToken>() { - - }.getType(); - final Set supportedParameters = new HashSet<>( - Arrays.asList(LoanApiConstants.idParameterName, LoanApiConstants.chargeIdParameterName, - LoanApiConstants.amountParameterName, LoanApiConstants.chargeTimeTypeParameterName, - LoanApiConstants.chargeCalculationTypeParameterName, LoanApiConstants.dueDateParamName)); - - final JsonArray array = topLevelJsonElement.get(LoanApiConstants.chargesParameterName).getAsJsonArray(); - for (int i = 1; i <= array.size(); i++) { - - final JsonObject loanChargeElement = array.get(i - 1).getAsJsonObject(); - final String arrayObjectJson = this.fromApiJsonHelper.toJson(loanChargeElement); - this.fromApiJsonHelper.checkForUnsupportedParameters(arrayObjectParameterTypeOfMap, arrayObjectJson, - supportedParameters); - - final Long chargeId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.chargeIdParameterName, - loanChargeElement); - baseDataValidator.reset().parameter(LoanApiConstants.chargesParameterName) - .parameterAtIndexArray(LoanApiConstants.chargeIdParameterName, i).value(chargeId).notNull() - .integerGreaterThanZero(); - - final BigDecimal amount = this.fromApiJsonHelper.extractBigDecimalNamed(LoanApiConstants.amountParameterName, - loanChargeElement, locale); - baseDataValidator.reset().parameter(LoanApiConstants.chargesParameterName) - .parameterAtIndexArray(LoanApiConstants.amountParameterName, i).value(amount).notNull().positiveAmount(); - - this.fromApiJsonHelper.extractLocalDateNamed(LoanApiConstants.dueDateParamName, loanChargeElement, dateFormat, locale); - } - } - } + loanChargeApiJsonValidator.validateLoanCharges(element, loanProduct, baseDataValidator); final String loanTypeStr = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.loanTypeParameterName, element); baseDataValidator.reset().parameter(LoanApiConstants.loanTypeParameterName).value(loanTypeStr).notNull(); @@ -1078,18 +1255,19 @@ public final class LoanApplicationCommandFromApiJsonHelper { baseDataValidator.reset().anyOfNotNull(forceError); } - if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.emiAmountParameterName, element)) { + if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.fixedEmiAmountParameterName, element)) { if (!(loanProduct.isCanDefineInstallmentAmount() || loanProduct.isMultiDisburseLoan())) { List unsupportedParameterList = new ArrayList<>(); - unsupportedParameterList.add(LoanApiConstants.emiAmountParameterName); + unsupportedParameterList.add(LoanApiConstants.fixedEmiAmountParameterName); throw new UnsupportedParameterException(unsupportedParameterList); } if (isEqualAmortization) { throw new EqualAmortizationUnsupportedFeatureException("fixed.emi", "fixed emi"); } - final BigDecimal emiAnount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.emiAmountParameterName, - element); - baseDataValidator.reset().parameter(LoanApiConstants.emiAmountParameterName).value(emiAnount).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)) { @@ -1106,13 +1284,55 @@ public final class LoanApplicationCommandFromApiJsonHelper { if (isTopup != null && isTopup) { final Long loanId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.loanIdToClose, element); baseDataValidator.reset().parameter(LoanApiConstants.loanIdToClose).value(loanId).notNull().longGreaterThanZero(); + + LocalDate submittedOnDate = this.fromApiJsonHelper.extractLocalDateNamed(LoanApiConstants.submittedOnDateParameterName, + element); + if (submittedOnDate == null) { + submittedOnDate = loan.getSubmittedOnDate(); + } + final Long loanIdToClose = command.longValueOfParameterNamed(LoanApiConstants.loanIdToClose); + final Loan loanToClose = this.loanRepositoryWrapper.findNonClosedLoanThatBelongsToClient(loanIdToClose, clientId); + if (loanToClose == null) { + throw new GeneralPlatformDomainRuleException("error.msg.loan.loanIdToClose.no.active.loan.associated.to.client.found", + "loanIdToClose is invalid, No Active Loan associated with the given Client ID found."); + } + if (loanToClose.isMultiDisburmentLoan() && !loanToClose.isInterestRecalculationEnabledForProduct()) { + throw new GeneralPlatformDomainRuleException( + "error.msg.loan.topup.on.multi.tranche.loan.without.interest.recalculation.not.supported", + "Topup on loan with multi-tranche disbursal and without interest recalculation is not supported."); + } + final LocalDate disbursalDateOfLoanToClose = loanToClose.getDisbursementDate(); + if (!DateUtils.isAfter(submittedOnDate, disbursalDateOfLoanToClose)) { + throw new GeneralPlatformDomainRuleException("error.msg.loan.submitted.date.should.be.after.topup.loan.disbursal.date", + "Submitted date of this loan application " + submittedOnDate + + " should be after the disbursed date of loan to be closed " + disbursalDateOfLoanToClose); + } + if (!loanToClose.getCurrencyCode().equals(loanProduct.getCurrency().getCode())) { + throw new GeneralPlatformDomainRuleException("error.msg.loan.to.be.closed.has.different.currency", + "loanIdToClose is invalid, Currency code is different."); + } + final LocalDate lastUserTransactionOnLoanToClose = loanToClose.getLastUserTransactionDate(); + if (DateUtils.isBefore(expectedDisbursementDate, lastUserTransactionOnLoanToClose)) { + throw new GeneralPlatformDomainRuleException( + "error.msg.loan.disbursal.date.should.be.after.last.transaction.date.of.loan.to.be.closed", + "Disbursal date of this loan application " + expectedDisbursementDate + + " should be after last transaction date of loan to be closed " + lastUserTransactionOnLoanToClose); + } + BigDecimal loanOutstanding = this.loanReadPlatformService + .retrieveLoanPrePaymentTemplate(LoanTransactionType.REPAYMENT, loanIdToClose, expectedDisbursementDate).getAmount(); + final BigDecimal firstDisbursalAmount = loan.getFirstDisbursalAmount(); + if (loanOutstanding.compareTo(firstDisbursalAmount) > 0) { + throw new GeneralPlatformDomainRuleException("error.msg.loan.amount.less.than.outstanding.of.loan.to.be.closed", + "Topup loan amount should be greater than outstanding amount of loan to be closed."); + } + } } validateLoanMultiDisbursementDate(element, baseDataValidator, expectedDisbursementDate, principal); validatePartialPeriodSupport(interestCalculationPeriodType, baseDataValidator, element, loanProduct); - String loanScheduleProcessingType = existingLoanApplication.getLoanRepaymentScheduleDetail().getLoanScheduleProcessingType().name(); + 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); @@ -1148,16 +1368,79 @@ public final class LoanApplicationCommandFromApiJsonHelper { } } + validateDisbursementDetails(loanProduct, element); + validateSubmittedOnDate(element, loan.getSubmittedOnDate(), loanProduct); + + validateClientOrGroup(client, group, productId); + + // validate if disbursement date is a holiday or a non-working day + validateDisbursementDateIsOnNonWorkingDay(expectedDisbursementDate); + Long officeId = client != null ? client.getOffice().getId() : group.getOffice().getId(); + validateDisbursementDateIsOnHoliday(expectedDisbursementDate, officeId); + + Integer recurringMoratoriumOnPrincipalPeriods = loan.getLoanProductRelatedDetail().getRecurringMoratoriumOnPrincipalPeriods(); + if (this.fromApiJsonHelper.parameterExists("recurringMoratoriumOnPrincipalPeriods", element)) { + recurringMoratoriumOnPrincipalPeriods = this.fromApiJsonHelper + .extractIntegerWithLocaleNamed("recurringMoratoriumOnPrincipalPeriods", element); + } + validateBorrowerCycle(element, loanProduct, clientId, groupId, baseDataValidator); + + loanProductDataValidator.validateRepaymentPeriodWithGraceSettings(numberOfRepayments, graceOnPrincipalPayment, + graceOnInterestPayment, graceOnInterestCharged, recurringMoratoriumOnPrincipalPeriods, baseDataValidator); + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", dataValidationErrors); } } - public void validateForUndo(final String json) { - if (StringUtils.isBlank(json)) { - throw new InvalidJsonException(); + private void validateClientOrGroup(Client client, Group group, Long productId) { + if (client != null) { + officeSpecificLoanProductValidation(productId, client.getOffice().getId()); + if (client.isNotActive()) { + throw new ClientNotActiveException(client.getId()); + } } + if (group != null) { + officeSpecificLoanProductValidation(productId, group.getOffice().getId()); + if (group.isNotActive()) { + throw new GroupNotActiveException(group.getId()); + } + } + + if (client != null && group != null) { + if (!group.hasClientAsMember(client)) { + throw new ClientNotInGroupException(client.getId(), group.getId()); + } + } + } + + private void validateDisbursementDetails(LoanProduct loanProduct, JsonElement element) { + if (loanProduct.isMultiDisburseLoan()) { + final JsonArray disbursementDataArray = this.fromApiJsonHelper + .extractJsonArrayNamed(LoanApiConstants.disbursementDataParameterName, element); + int disbursementDataSize = disbursementDataArray != null ? disbursementDataArray.size() : 0; + if (loanProduct.isDisallowExpectedDisbursements()) { + if (disbursementDataSize > 0) { + final String errorMessage = "For this loan product, disbursement details are not allowed"; + throw new MultiDisbursementDataNotAllowedException(LoanApiConstants.disbursementDataParameterName, errorMessage); + } + } else { + if (disbursementDataSize == 0) { + final String errorMessage = "For this loan product, disbursement details must be provided"; + throw new MultiDisbursementDataRequiredException(LoanApiConstants.disbursementDataParameterName, errorMessage); + } + } + if (disbursementDataSize > loanProduct.maxTrancheCount()) { + final String errorMessage = "Number of tranche shouldn't be greter than " + loanProduct.maxTrancheCount(); + throw new ExceedingTrancheCountException(LoanApiConstants.disbursementDataParameterName, errorMessage, + loanProduct.maxTrancheCount(), disbursementDataSize); + } + } + } + + public void validateForUndo(final String json) { + validateRequestBody(json); final Set undoSupportedParameters = new HashSet<>(List.of(LoanApiConstants.noteParamName)); final Type typeOfMap = new TypeToken>() { @@ -1201,16 +1484,17 @@ public final class LoanApplicationCommandFromApiJsonHelper { } } - public void validateLoanTermAndRepaidEveryValues(final Integer loanTermFrequency, final Integer loanTermFrequencyType, + private void validateLoanTermAndRepaidEveryValues(final Integer loanTermFrequency, final Integer loanTermFrequencyType, final Integer numberOfRepayments, final Integer repaymentEvery, final Integer repaymentEveryType, final Loan loan) { final List dataValidationErrors = new ArrayList<>(); - this.apiJsonHelper.validateSelectedPeriodFrequencyTypeIsTheSame(dataValidationErrors, loanTermFrequency, loanTermFrequencyType, - numberOfRepayments, repaymentEvery, repaymentEveryType); + 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(); Integer maximumNoOfRepayments = loan.loanProduct().getMaxNumberOfRepayments(); @@ -1244,23 +1528,29 @@ public final class LoanApplicationCommandFromApiJsonHelper { } } - public void validatelinkedSavingsAccount(final SavingsAccount savingsAccount, final Loan loanApplication) { - final List dataValidationErrors = new ArrayList<>(); - if (savingsAccount.isNotActive()) { - final ApiParameterError error = ApiParameterError.parameterError("validation.msg.loan.linked.savings.account.is.not.active", - "Linked Savings account with id:" + savingsAccount.getId() + " is not in active state", "linkAccountId", - savingsAccount.getId()); - dataValidationErrors.add(error); - } else if (!loanApplication.getClientId().equals(savingsAccount.clientId())) { - final ApiParameterError error = ApiParameterError.parameterError( - "validation.msg.loan.linked.savings.account.not.belongs.to.same.client", - "Linked Savings account with id:" + savingsAccount.getId() + " is not belongs to the same client", "linkAccountId", - savingsAccount.getId()); - dataValidationErrors.add(error); - } - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", - dataValidationErrors); + public void validateLinkedSavingsAccount(final JsonElement element, DataValidatorBuilder baseDataValidator) { + final boolean linkedAccountIdWasProvided = this.fromApiJsonHelper.parameterExists(LoanApiConstants.linkAccountIdParameterName, + element); + if (linkedAccountIdWasProvided) { + final Long linkAccountId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.linkAccountIdParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.linkAccountIdParameterName).value(linkAccountId).ignoreIfNull() + .longGreaterThanZero(); + + final Long linkedAccountId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.linkAccountIdParameterName, element); + final SavingsAccount savingsAccount = savingsAccountRepository.findOneWithNotFoundDetection(linkedAccountId); + final Long clientId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.clientIdParameterName, element); + if (savingsAccount.isNotActive()) { + final ApiParameterError error = ApiParameterError.parameterError("validation.msg.loan.linked.savings.account.is.not.active", + "Linked Savings account with id:" + savingsAccount.getId() + " is not in active state", "linkAccountId", + savingsAccount.getId()); + baseDataValidator.getDataValidationErrors().add(error); + } else if (!clientId.equals(savingsAccount.clientId())) { + final ApiParameterError error = ApiParameterError.parameterError( + "validation.msg.loan.linked.savings.account.not.belongs.to.same.client", + "Linked Savings account with id:" + savingsAccount.getId() + " is not belongs to the same client", "linkAccountId", + savingsAccount.getId()); + baseDataValidator.getDataValidationErrors().add(error); + } } } @@ -1364,15 +1654,6 @@ public final class LoanApplicationCommandFromApiJsonHelper { } - public void validateLoanForInterestRecalculation(final Loan loan) { - final List dataValidationErrors = new ArrayList<>(); - - loanChargeApiJsonValidator.validateLoanCharges(loan.getActiveCharges(), dataValidationErrors); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - public void validateLoanForCollaterals(final Loan loan, final BigDecimal total) { String errorCode; final List dataValidationErrors = new ArrayList<>(); @@ -1436,4 +1717,164 @@ public final class LoanApplicationCommandFromApiJsonHelper { } } + private void officeSpecificLoanProductValidation(final Long productId, final Long officeId) { + final GlobalConfigurationProperty restrictToUserOfficeProperty = this.globalConfigurationRepository + .findOneByNameWithNotFoundDetection(FineractEntityAccessConstants.GLOBAL_CONFIG_FOR_OFFICE_SPECIFIC_PRODUCTS); + if (restrictToUserOfficeProperty.isEnabled()) { + FineractEntityRelation fineractEntityRelation = fineractEntityRelationRepository + .findOneByCodeName(FineractEntityAccessType.OFFICE_ACCESS_TO_LOAN_PRODUCTS.getStr()); + FineractEntityToEntityMapping officeToLoanProductMappingList = this.entityMappingRepository + .findListByProductId(fineractEntityRelation, productId, officeId); + if (officeToLoanProductMappingList == null) { + throw new NotOfficeSpecificProductException(productId, officeId); + } + + } + } + + private void validateTransactionProcessingStrategy(final String transactionProcessingStrategy, final LoanProduct loanProduct, + final DataValidatorBuilder baseDataValidator) { + + baseDataValidator.reset().parameter(LoanApiConstants.transactionProcessingStrategyCodeParameterName) + .value(transactionProcessingStrategy).notNull(); + + // TODO: Review exceptions + 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"); + } else { + // PROGRESSIVE: Repayment strategy MUST be only "advanced payment allocation" + if (loanProduct.getLoanProductRelatedDetail().getLoanScheduleType().equals(LoanScheduleType.PROGRESSIVE)) { + if (!transactionProcessingStrategy.equals(LoanProductConstants.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)) { + // TODO: GeneralPlatformDomainRuleException vs PlatformApiDataValidationException + throw new GeneralPlatformDomainRuleException( + "error.msg.loan.repayment.strategy.can.not.be.different.than.advanced.payment.allocation", + "Loan repayment strategy can not be different than Advanced Payment Allocation"); + } + // CUMULATIVE: Repayment strategy CANNOT be "advanced payment allocation" + } else if (loanProduct.getLoanProductRelatedDetail().getLoanScheduleType().equals(LoanScheduleType.CUMULATIVE)) { + if (transactionProcessingStrategy.equals(LoanProductConstants.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)) { + // TODO: GeneralPlatformDomainRuleException vs PlatformApiDataValidationException + throw new GeneralPlatformDomainRuleException( + "error.msg.loan.repayment.strategy.can.not.be.equal.to.advanced.payment.allocation", + "Loan repayment strategy can not be equal to Advanced Payment Allocation"); + } + } + } + // Validating whether the processor is existing + loanRepaymentScheduleTransactionProcessorFactory.determineProcessor(transactionProcessingStrategy); + } + + public void checkForProductMixRestrictions(final JsonElement element) { + + final List activeLoansLoanProductIds; + final Long productId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.productIdParameterName, element); + final Long groupId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.groupIdParameterName, element); + final Long clientId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.clientIdParameterName, element); + + if (groupId != null) { + activeLoansLoanProductIds = this.loanRepositoryWrapper.findActiveLoansLoanProductIdsByGroup(groupId, + LoanStatus.ACTIVE.getValue()); + } else { + activeLoansLoanProductIds = this.loanRepositoryWrapper.findActiveLoansLoanProductIdsByClient(clientId, + LoanStatus.ACTIVE.getValue()); + } + checkForProductMixRestrictions(activeLoansLoanProductIds, productId); + } + + private void checkForProductMixRestrictions(final List activeLoansLoanProductIds, final Long productId) { + + if (!CollectionUtils.isEmpty(activeLoansLoanProductIds)) { + final Collection restrictedProductsList = this.loanProductReadPlatformService + .retrieveRestrictedProductsForMix(productId); + for (final LoanProductData restrictedProduct : restrictedProductsList) { + if (activeLoansLoanProductIds.contains(restrictedProduct.getId())) { + throw new GeneralPlatformDomainRuleException( + "error.msg.loan.applied.or.to.be.disbursed.can.not.co-exist.with.the.loan.already.active.to.this.client", + "This loan could not be applied/disbursed as the loan and `" + restrictedProduct.getName() + + "` are not allowed to co-exist"); + } + } + } + } + + private void validateSubmittedOnDate(final JsonElement element, LocalDate originalSubmittedOnDate, LoanProduct loanProduct) { + final LocalDate startDate = loanProduct.getStartDate(); + final LocalDate closeDate = loanProduct.getCloseDate(); + final LocalDate submittedOnDate = this.fromApiJsonHelper.parameterExists(LoanApiConstants.submittedOnDateParameterName, element) + ? this.fromApiJsonHelper.extractLocalDateNamed(LoanApiConstants.submittedOnDateParameterName, element) + : originalSubmittedOnDate; + final Long clientId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.clientIdParameterName, element); + final Long groupId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.groupIdParameterName, element); + final LocalDate expectedDisbursementDate = this.fromApiJsonHelper + .extractLocalDateNamed(LoanApiConstants.expectedDisbursementDateParameterName, element); + + String defaultUserMessage = ""; + if (DateUtils.isBefore(submittedOnDate, startDate)) { + defaultUserMessage = "submittedOnDate cannot be before the loan product startDate."; + throw new LoanApplicationDateException("submitted.on.date.cannot.be.before.the.loan.product.start.date", defaultUserMessage, + submittedOnDate.toString(), startDate.toString()); + } + + if (closeDate != null && DateUtils.isAfter(submittedOnDate, closeDate)) { + defaultUserMessage = "submittedOnDate cannot be after the loan product closeDate."; + throw new LoanApplicationDateException("submitted.on.date.cannot.be.after.the.loan.product.close.date", defaultUserMessage, + submittedOnDate.toString(), closeDate.toString()); + } + + // TODO: Common exception handling would be nice + if (DateUtils.isDateInTheFuture(submittedOnDate)) { + final String errorMessage = "The date on which a loan is submitted cannot be in the future."; + throw new InvalidLoanStateTransitionException("submittal", "cannot.be.a.future.date", errorMessage, submittedOnDate, + DateUtils.getBusinessLocalDate()); + } + + if (clientId != null) { + Client client = clientRepository.findOneWithNotFoundDetection(clientId); + if (client != null && client.isActivatedAfter(submittedOnDate)) { + final String errorMessage = "The date on which a loan is submitted cannot be earlier than client's activation date."; + throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.client.activation.date", errorMessage, + submittedOnDate, client.getActivationDate()); + } + if (client != null && client.getOfficeJoiningDate() != null + && DateUtils.isBefore(submittedOnDate, client.getOfficeJoiningDate())) { + throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.client.transfer.date", + "The date on which a loan is submitted cannot be earlier than client's transfer date to this office", + client.getOfficeJoiningDate()); + } + + } + if (groupId != null) { + Group group = groupRepository.findOneWithNotFoundDetection(groupId); + + if (group != null && group.isActivatedAfter(submittedOnDate)) { + final String errorMessage = "The date on which a loan is submitted cannot be earlier than groups's activation date."; + throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.group.activation.date", errorMessage, + submittedOnDate, group.getActivationDate()); + } + + if (DateUtils.isAfter(submittedOnDate, expectedDisbursementDate)) { + final String errorMessage = "The date on which a loan is submitted cannot be after its expected disbursement date: " + + expectedDisbursementDate; + throw new InvalidLoanStateTransitionException("submittal", "cannot.be.after.expected.disbursement.date", errorMessage, + submittedOnDate, expectedDisbursementDate); + } + } + } + + private static void validateRequestBody(String json) { + if (StringUtils.isBlank(json)) { + throw new InvalidJsonException(); + } + } + + private void validateForSupportedParameters(String json) { + final Type typeOfMap = new TypeToken>() { + + }.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, SUPPORTED_PARAMETERS); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanChargeApiJsonValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanChargeApiJsonValidator.java index f9149a9e0..1d0e0703e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanChargeApiJsonValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanChargeApiJsonValidator.java @@ -18,7 +18,9 @@ */ package org.apache.fineract.portfolio.loanaccount.serialization; +import com.google.gson.JsonArray; import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import com.google.gson.reflect.TypeToken; import java.lang.reflect.Type; import java.math.BigDecimal; @@ -27,6 +29,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; import lombok.AllArgsConstructor; @@ -36,8 +39,20 @@ import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; import org.apache.fineract.infrastructure.core.exception.InvalidJsonException; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.organisation.monetary.exception.InvalidCurrencyException; +import org.apache.fineract.portfolio.charge.domain.Charge; +import org.apache.fineract.portfolio.charge.domain.ChargeCalculationType; +import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode; +import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper; +import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; +import org.apache.fineract.portfolio.charge.exception.LoanChargeCannotBeAddedException; +import org.apache.fineract.portfolio.charge.exception.LoanChargeNotFoundException; import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; +import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeRepository; +import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; +import org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException; import org.springframework.stereotype.Component; @Component @@ -45,6 +60,8 @@ import org.springframework.stereotype.Component; public final class LoanChargeApiJsonValidator { private final FromJsonHelper fromApiJsonHelper; + private final ChargeRepositoryWrapper chargeRepository; + private final LoanChargeRepository loanChargeRepository; private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { if (!dataValidationErrors.isEmpty()) { @@ -274,4 +291,162 @@ public final class LoanChargeApiJsonValidator { } } } + + public void validateLoanCharges(JsonElement element, LoanProduct loanProduct, DataValidatorBuilder baseDataValidator) { + if (element.isJsonObject() && this.fromApiJsonHelper.parameterExists(LoanApiConstants.chargesParameterName, element)) { + final JsonObject topLevelJsonElement = element.getAsJsonObject(); + final String dateFormat = this.fromApiJsonHelper.extractDateFormatParameter(topLevelJsonElement); + final Locale locale = this.fromApiJsonHelper.extractLocaleParameter(topLevelJsonElement); + + if (topLevelJsonElement.get(LoanApiConstants.chargesParameterName).isJsonArray()) { + final Type arrayObjectParameterTypeOfMap = new TypeToken>() { + + }.getType(); + final Set supportedParameters = new HashSet<>( + Arrays.asList(LoanApiConstants.idParameterName, LoanApiConstants.chargeIdParameterName, + LoanApiConstants.amountParameterName, LoanApiConstants.chargeTimeTypeParameterName, + LoanApiConstants.chargeCalculationTypeParameterName, LoanApiConstants.dueDateParamName, + LoanApiConstants.chargePaymentModeParameterName, LoanApiConstants.externalIdParameterName)); + + final JsonArray array = topLevelJsonElement.get(LoanApiConstants.chargesParameterName).getAsJsonArray(); + for (int i = 1; i <= array.size(); i++) { + final JsonObject loanChargeElement = array.get(i - 1).getAsJsonObject(); + final String arrayObjectJson = this.fromApiJsonHelper.toJson(loanChargeElement); + this.fromApiJsonHelper.checkForUnsupportedParameters(arrayObjectParameterTypeOfMap, arrayObjectJson, + supportedParameters); + + String chargeCurrencyCode; + Charge chargeDefinition; + ChargeTimeType chargeTime; + ChargeCalculationType chargeCalculation; + ChargePaymentMode chargePaymentModeEnum; + final Integer chargeTimeType = this.fromApiJsonHelper.extractIntegerNamed("chargeTimeType", loanChargeElement, locale); + final Integer chargeCalculationType = this.fromApiJsonHelper.extractIntegerNamed("chargeCalculationType", + loanChargeElement, locale); + + final Integer chargePaymentMode = this.fromApiJsonHelper.extractIntegerNamed("chargePaymentMode", loanChargeElement, + locale); + final Long chargeId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.chargeIdParameterName, + loanChargeElement); + final Long loanChargeId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.idParameterName, element); + if (loanChargeId == null) { + baseDataValidator.reset().parameter(LoanApiConstants.chargesParameterName) + .parameterAtIndexArray(LoanApiConstants.chargeIdParameterName, i).value(chargeId).notNull() + .integerGreaterThanZero(); + + } else if (chargeId == null) { + baseDataValidator.reset().parameter(LoanApiConstants.idParameterName) + .parameterAtIndexArray(LoanApiConstants.idParameterName, i).value(loanChargeId).notNull() + .integerGreaterThanZero(); + } + + if (loanChargeId == null && chargeId == null) { + return; + } + if (chargeId != null) { + + chargeDefinition = this.chargeRepository.findOneWithNotFoundDetection(chargeId); + chargeCurrencyCode = chargeDefinition.getCurrencyCode(); + if (chargeTimeType != null) { + chargeTime = ChargeTimeType.fromInt(chargeTimeType); + } else { + chargeTime = ChargeTimeType.fromInt(chargeDefinition.getChargeTimeType()); + } + + if (chargeCalculationType != null) { + chargeCalculation = ChargeCalculationType.fromInt(chargeCalculationType); + } else { + chargeCalculation = ChargeCalculationType.fromInt(chargeDefinition.getChargeCalculation()); + } + + if (chargePaymentMode != null) { + chargePaymentModeEnum = ChargePaymentMode.fromInt(chargePaymentMode); + } else { + chargePaymentModeEnum = ChargePaymentMode.fromInt(chargeDefinition.getChargePaymentMode()); + } + } else { + LoanCharge loanCharge = this.loanChargeRepository.findById(loanChargeId) + .orElseThrow(() -> new LoanChargeNotFoundException(loanChargeId)); + chargeDefinition = loanCharge.getCharge(); + chargeCurrencyCode = chargeDefinition.getCurrencyCode(); + chargeTime = loanCharge.getChargeTimeType(); + chargeCalculation = loanCharge.getChargeCalculation(); + chargePaymentModeEnum = loanCharge.getChargePaymentMode(); + } + + if (!loanProduct.hasCurrencyCodeOf(chargeCurrencyCode)) { + final String errorMessage = "Charge and Loan must have the same currency."; + // TODO: GeneralPlatformDomainRuleException vs PlatformApiDataValidationException + throw new InvalidCurrencyException("loanCharge", "attach.to.loan", errorMessage); + } + if (chargeDefinition.isOverdueInstallment()) { + final String defaultUserMessage = "Installment charge cannot be added to the loan."; + throw new LoanChargeCannotBeAddedException("loanCharge", "overdue.charge", defaultUserMessage, null, + chargeDefinition.getName()); + } + final BigDecimal amount = this.fromApiJsonHelper.extractBigDecimalNamed(LoanApiConstants.amountParameterName, + loanChargeElement, locale); + baseDataValidator.reset().parameter(LoanApiConstants.chargesParameterName) + .parameterAtIndexArray(LoanApiConstants.amountParameterName, i).value(amount).notNull().positiveAmount(); + + if (chargeTime.isSpecifiedDueDate()) { + LocalDate dueDate = this.fromApiJsonHelper.extractLocalDateNamed(LoanApiConstants.dueDateParamName, + loanChargeElement, dateFormat, locale); + LocalDate expectedDisbursementDate = this.fromApiJsonHelper + .extractLocalDateNamed(LoanApiConstants.expectedDisbursementDateParameterName, element); + if (DateUtils.isBefore(dueDate, expectedDisbursementDate)) { + final String defaultUserMessage = "This charge with specified due date cannot be added as the it is not in schedule range."; + throw new LoanChargeCannotBeAddedException("loanCharge", "specified.due.date.outside.range", defaultUserMessage, + expectedDisbursementDate, chargeDefinition.getName()); + } + } + + if (chargePaymentMode != null) { + if (chargePaymentModeEnum.isPaymentModeAccountTransfer()) { + final Long savingsAccountId = this.fromApiJsonHelper.extractLongNamed("linkAccountId", element); + if (savingsAccountId == null) { + final String errorMessage = "one of the charges requires linked savings account for payment"; + throw new LinkedAccountRequiredException("loanCharge", errorMessage); + } + } + } + + validateInterestBearingLoanProductRestriction(chargeCalculation, chargeTime, loanProduct, baseDataValidator); + } + } + } + } + + private void validateInterestBearingLoanProductRestriction(ChargeCalculationType chargeCalculationType, ChargeTimeType chargeTime, + LoanProduct loanProduct, DataValidatorBuilder baseDataValidator) { + if (loanProduct.isInterestRecalculationEnabled()) { + String errorcode = null; + switch (chargeCalculationType) { + case PERCENT_OF_AMOUNT: + if (chargeTime.isInstalmentFee()) { + errorcode = "installment." + LoanApiConstants.LOAN_CHARGE_CAN_NOT_BE_ADDED_WITH_PRINCIPAL_CALCULATION_TYPE; + + } + break; + case PERCENT_OF_AMOUNT_AND_INTEREST: + if (chargeTime.isInstalmentFee()) { + errorcode = "installment." + LoanApiConstants.LOAN_CHARGE_CAN_NOT_BE_ADDED_WITH_PRINCIPAL_CALCULATION_TYPE; + } else if (chargeTime.isSpecifiedDueDate()) { + errorcode = "specific." + LoanApiConstants.LOAN_CHARGE_CAN_NOT_BE_ADDED_WITH_INTEREST_CALCULATION_TYPE; + } + break; + case PERCENT_OF_INTEREST: + if (chargeTime.isSpecifiedDueDate()) { + errorcode = "specific." + LoanApiConstants.LOAN_CHARGE_CAN_NOT_BE_ADDED_WITH_INTEREST_CALCULATION_TYPE; + } + break; + + default: + break; + } + if (errorcode != null) { + baseDataValidator.reset().parameter("charges").failWithCode(errorcode); + } + } + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java index b95a5d54f..e26c0f2df 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java @@ -59,7 +59,7 @@ import org.springframework.stereotype.Component; public final class LoanEventApiJsonValidator { private final FromJsonHelper fromApiJsonHelper; - private final LoanApplicationCommandFromApiJsonHelper fromApiJsonDeserializer; + private final LoanApplicationValidator fromApiJsonDeserializer; private final LoanRepository loanRepository; private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { @@ -79,12 +79,12 @@ public final class LoanEventApiJsonValidator { if (isAccountTransfer) { disbursementParameters = new HashSet<>(Arrays.asList("actualDisbursementDate", "externalId", "note", "locale", "dateFormat", - LoanApiConstants.principalDisbursedParameterName, LoanApiConstants.emiAmountParameterName, + LoanApiConstants.principalDisbursedParameterName, LoanApiConstants.fixedEmiAmountParameterName, LoanApiConstants.disbursementNetDisbursalAmountParameterName)); } else { disbursementParameters = new HashSet<>(Arrays.asList("actualDisbursementDate", "externalId", "note", "locale", "dateFormat", "paymentTypeId", "accountNumber", "checkNumber", "routingCode", "receiptNumber", "bankNumber", "adjustRepaymentDate", - LoanApiConstants.principalDisbursedParameterName, LoanApiConstants.emiAmountParameterName, + LoanApiConstants.principalDisbursedParameterName, LoanApiConstants.fixedEmiAmountParameterName, LoanApiConstants.postDatedChecks, LoanApiConstants.disbursementNetDisbursalAmountParameterName)); } @@ -111,9 +111,9 @@ public final class LoanEventApiJsonValidator { baseDataValidator.reset().parameter(LoanApiConstants.disbursementNetDisbursalAmountParameterName).value(netDisbursalAmount) .ignoreIfNull().positiveAmount(); - final BigDecimal emiAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.emiAmountParameterName, + final BigDecimal emiAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.fixedEmiAmountParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.emiAmountParameterName).value(emiAmount).ignoreIfNull().positiveAmount() + baseDataValidator.reset().parameter(LoanApiConstants.fixedEmiAmountParameterName).value(emiAmount).ignoreIfNull().positiveAmount() .notGreaterThanMax(principal); validatePaymentDetails(baseDataValidator, element); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/CalculateLoanScheduleQueryFromApiJsonHelper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanScheduleValidator.java similarity index 98% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/CalculateLoanScheduleQueryFromApiJsonHelper.java rename to fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanScheduleValidator.java index 850e25ce6..d9167b633 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/CalculateLoanScheduleQueryFromApiJsonHelper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanScheduleValidator.java @@ -40,7 +40,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component -public final class CalculateLoanScheduleQueryFromApiJsonHelper { +public final class LoanScheduleValidator { /** * The parameters supported for this command. @@ -64,7 +64,7 @@ public final class CalculateLoanScheduleQueryFromApiJsonHelper { LoanApiConstants.submittedOnNoteParameterName, LoanApiConstants.localeParameterName, LoanApiConstants.dateFormatParameterName, LoanApiConstants.chargesParameterName, LoanApiConstants.collateralParameterName, LoanApiConstants.syncDisbursementWithMeetingParameterName, LoanApiConstants.linkAccountIdParameterName, - LoanApiConstants.disbursementDataParameterName, LoanApiConstants.emiAmountParameterName, + LoanApiConstants.disbursementDataParameterName, LoanApiConstants.fixedEmiAmountParameterName, LoanApiConstants.maxOutstandingBalanceParameterName, LoanProductConstants.GRACE_ON_ARREARS_AGEING_PARAMETER_NAME, LoanApiConstants.createStandingInstructionAtDisbursementParameterName, LoanApiConstants.isFloatingInterestRateParameterName, LoanApiConstants.interestRateDifferentialParameterName, LoanApiConstants.repaymentFrequencyNthDayTypeParameterName, @@ -76,7 +76,7 @@ public final class CalculateLoanScheduleQueryFromApiJsonHelper { private final FromJsonHelper fromApiJsonHelper; @Autowired - public CalculateLoanScheduleQueryFromApiJsonHelper(final FromJsonHelper fromApiJsonHelper) { + public LoanScheduleValidator(final FromJsonHelper fromApiJsonHelper) { this.fromApiJsonHelper = fromApiJsonHelper; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/GLIMAccountInfoWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/GLIMAccountInfoWritePlatformService.java index 042a1ff2b..8fa3e09b9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/GLIMAccountInfoWritePlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/GLIMAccountInfoWritePlatformService.java @@ -31,6 +31,6 @@ public interface GLIMAccountInfoWritePlatformService { void incrementChildAccountCount(GroupLoanIndividualMonitoringAccount glimAccount); - void addGLIMAccountInfo(String accountNumber, Group group, BigDecimal principalAmount, Long childAccountsCount, - Boolean isAcceptingChild, Integer loanStatus, BigDecimal applicationId); + GroupLoanIndividualMonitoringAccount createGLIMAccount(String accountNumber, Group group, BigDecimal principalAmount, + Long childAccountsCount, Boolean isAcceptingChild, Integer loanStatus, BigDecimal applicationId); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/GLIMAccountInfoWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/GLIMAccountInfoWritePlatformServiceImpl.java index f07da8036..b4329c588 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/GLIMAccountInfoWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/GLIMAccountInfoWritePlatformServiceImpl.java @@ -21,30 +21,23 @@ package org.apache.fineract.portfolio.loanaccount.service; import java.math.BigDecimal; import lombok.RequiredArgsConstructor; -import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.portfolio.group.domain.Group; import org.apache.fineract.portfolio.loanaccount.domain.GLIMAccountInfoRepository; import org.apache.fineract.portfolio.loanaccount.domain.GroupLoanIndividualMonitoringAccount; -import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; @RequiredArgsConstructor public class GLIMAccountInfoWritePlatformServiceImpl implements GLIMAccountInfoWritePlatformService { - private final PlatformSecurityContext context; - private final GLIMAccountInfoRepository glimAccountRepository; - private final LoanRepository loanRepository; - @Override - public void addGLIMAccountInfo(String accountNumber, Group group, BigDecimal principalAmount, Long childAccountsCount, - Boolean isAcceptingChild, Integer loanStatus, BigDecimal applicationId) { + public GroupLoanIndividualMonitoringAccount createGLIMAccount(String accountNumber, Group group, BigDecimal principalAmount, + Long childAccountsCount, Boolean isAcceptingChild, Integer loanStatus, BigDecimal applicationId) { GroupLoanIndividualMonitoringAccount glimAccountInfo = GroupLoanIndividualMonitoringAccount.getInstance(accountNumber, group, principalAmount, childAccountsCount, isAcceptingChild, loanStatus, applicationId); - this.glimAccountRepository.save(glimAccountInfo); - + return this.glimAccountRepository.save(glimAccountInfo); } @Override 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 6d59184d9..995a1b5c4 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 @@ -26,7 +26,6 @@ import java.math.BigDecimal; import java.time.LocalDate; import java.time.temporal.ChronoField; import java.util.ArrayList; -import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -35,48 +34,30 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; -import org.apache.fineract.infrastructure.accountnumberformat.domain.AccountNumberFormat; -import org.apache.fineract.infrastructure.accountnumberformat.domain.AccountNumberFormatRepositoryWrapper; -import org.apache.fineract.infrastructure.accountnumberformat.domain.EntityAccountType; -import org.apache.fineract.infrastructure.codes.domain.CodeValue; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; -import org.apache.fineract.infrastructure.configuration.domain.GlobalConfigurationProperty; -import org.apache.fineract.infrastructure.configuration.domain.GlobalConfigurationRepositoryWrapper; import org.apache.fineract.infrastructure.core.api.JsonCommand; -import org.apache.fineract.infrastructure.core.api.JsonQuery; import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; -import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.exception.ErrorHandler; import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.service.DateUtils; -import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.infrastructure.dataqueries.data.EntityTables; import org.apache.fineract.infrastructure.dataqueries.data.StatusEnum; import org.apache.fineract.infrastructure.dataqueries.service.EntityDatatableChecksWritePlatformService; -import org.apache.fineract.infrastructure.entityaccess.FineractEntityAccessConstants; -import org.apache.fineract.infrastructure.entityaccess.domain.FineractEntityAccessType; -import org.apache.fineract.infrastructure.entityaccess.domain.FineractEntityRelation; -import org.apache.fineract.infrastructure.entityaccess.domain.FineractEntityRelationRepository; -import org.apache.fineract.infrastructure.entityaccess.domain.FineractEntityToEntityMapping; -import org.apache.fineract.infrastructure.entityaccess.domain.FineractEntityToEntityMappingRepository; -import org.apache.fineract.infrastructure.entityaccess.exception.NotOfficeSpecificProductException; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanApprovedBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanCreatedBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanRejectedBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanUndoApprovalBusinessEvent; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; -import org.apache.fineract.organisation.staff.domain.Staff; import org.apache.fineract.portfolio.account.domain.AccountAssociationType; import org.apache.fineract.portfolio.account.domain.AccountAssociations; import org.apache.fineract.portfolio.account.domain.AccountAssociationsRepository; -import org.apache.fineract.portfolio.accountdetails.domain.AccountType; import org.apache.fineract.portfolio.calendar.domain.Calendar; import org.apache.fineract.portfolio.calendar.domain.CalendarEntityType; import org.apache.fineract.portfolio.calendar.domain.CalendarFrequencyType; @@ -86,70 +67,43 @@ import org.apache.fineract.portfolio.calendar.domain.CalendarRepository; import org.apache.fineract.portfolio.calendar.domain.CalendarType; import org.apache.fineract.portfolio.calendar.exception.CalendarNotFoundException; import org.apache.fineract.portfolio.calendar.service.CalendarReadPlatformService; -import org.apache.fineract.portfolio.charge.domain.Charge; -import org.apache.fineract.portfolio.client.domain.AccountNumberGenerator; import org.apache.fineract.portfolio.client.domain.Client; -import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper; import org.apache.fineract.portfolio.client.exception.ClientNotActiveException; import org.apache.fineract.portfolio.collateralmanagement.domain.ClientCollateralManagement; -import org.apache.fineract.portfolio.collateralmanagement.service.LoanCollateralAssembler; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; -import org.apache.fineract.portfolio.fund.domain.Fund; import org.apache.fineract.portfolio.group.domain.Group; -import org.apache.fineract.portfolio.group.domain.GroupRepositoryWrapper; import org.apache.fineract.portfolio.group.exception.GroupMemberNotFoundInGSIMException; import org.apache.fineract.portfolio.group.exception.GroupNotActiveException; import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; -import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData; import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; import org.apache.fineract.portfolio.loanaccount.domain.GLIMAccountInfoRepository; import org.apache.fineract.portfolio.loanaccount.domain.GroupLoanIndividualMonitoringAccount; import org.apache.fineract.portfolio.loanaccount.domain.Loan; -import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagement; -import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails; import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; import org.apache.fineract.portfolio.loanaccount.domain.LoanSummaryWrapper; -import org.apache.fineract.portfolio.loanaccount.domain.LoanTopupDetails; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; -import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; -import org.apache.fineract.portfolio.loanaccount.exception.LoanApplicationDateException; import org.apache.fineract.portfolio.loanaccount.exception.LoanApplicationNotInSubmittedAndPendingApprovalStateCannotBeDeleted; -import org.apache.fineract.portfolio.loanaccount.exception.LoanApplicationNotInSubmittedAndPendingApprovalStateCannotBeModified; -import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.AprCalculator; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; -import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; -import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleAssembler; -import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleCalculationPlatformService; -import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationCommandFromApiJsonHelper; import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationTransitionApiJsonValidator; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationValidator; import org.apache.fineract.portfolio.loanproduct.LoanProductConstants; -import org.apache.fineract.portfolio.loanproduct.data.LoanProductData; -import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; -import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; -import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository; import org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType; -import org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException; -import org.apache.fineract.portfolio.loanproduct.exception.LoanProductNotFoundException; -import org.apache.fineract.portfolio.loanproduct.serialization.LoanProductDataValidator; -import org.apache.fineract.portfolio.loanproduct.service.LoanProductReadPlatformService; import org.apache.fineract.portfolio.note.domain.Note; import org.apache.fineract.portfolio.note.domain.NoteRepository; -import org.apache.fineract.portfolio.rate.service.RateAssembler; import org.apache.fineract.portfolio.savings.data.GroupSavingsIndividualMonitoringAccountData; import org.apache.fineract.portfolio.savings.domain.SavingsAccount; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper; import org.apache.fineract.portfolio.savings.service.GSIMReadPlatformService; import org.apache.fineract.useradministration.domain.AppUser; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.orm.jpa.JpaSystemException; import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.CollectionUtils; @Slf4j @RequiredArgsConstructor @@ -158,379 +112,77 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa private final PlatformSecurityContext context; private final FromJsonHelper fromJsonHelper; private final LoanApplicationTransitionApiJsonValidator loanApplicationTransitionApiJsonValidator; - private final LoanProductDataValidator loanProductCommandFromApiJsonDeserializer; - private final LoanApplicationCommandFromApiJsonHelper fromApiJsonDeserializer; + private final LoanApplicationValidator loanApplicationValidator; private final LoanRepositoryWrapper loanRepositoryWrapper; private final NoteRepository noteRepository; - private final LoanScheduleCalculationPlatformService calculationPlatformService; private final LoanAssembler loanAssembler; - private final ClientRepositoryWrapper clientRepository; - private final LoanProductRepository loanProductRepository; - private final LoanChargeAssembler loanChargeAssembler; - private final LoanCollateralAssembler loanCollateralAssembler; - private final AprCalculator aprCalculator; - private final AccountNumberGenerator accountNumberGenerator; private final LoanSummaryWrapper loanSummaryWrapper; - private final GroupRepositoryWrapper groupRepository; private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory; private final CalendarRepository calendarRepository; private final CalendarInstanceRepository calendarInstanceRepository; - private final SavingsAccountAssembler savingsAccountAssembler; + private final SavingsAccountRepositoryWrapper savingsAccountRepository; private final AccountAssociationsRepository accountAssociationsRepository; private final LoanReadPlatformService loanReadPlatformService; - private final AccountNumberFormatRepositoryWrapper accountNumberFormatRepository; private final BusinessEventNotifierService businessEventNotifierService; private final ConfigurationDomainService configurationDomainService; private final LoanScheduleAssembler loanScheduleAssembler; private final LoanUtilService loanUtilService; private final CalendarReadPlatformService calendarReadPlatformService; private final EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService; - private final GlobalConfigurationRepositoryWrapper globalConfigurationRepository; - private final FineractEntityToEntityMappingRepository entityMappingRepository; - private final FineractEntityRelationRepository fineractEntityRelationRepository; - private final LoanProductReadPlatformService loanProductReadPlatformService; - - private final RateAssembler rateAssembler; - private final GLIMAccountInfoWritePlatformService glimAccountInfoWritePlatformService; private final GLIMAccountInfoRepository glimRepository; private final LoanRepository loanRepository; private final GSIMReadPlatformService gsimReadPlatformService; private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine; - private final LoanProductDataValidator loanProductDataValidator; @Transactional @Override public CommandProcessingResult submitApplication(final JsonCommand command) { try { - boolean isMeetingMandatoryForJLGLoans = configurationDomainService.isMeetingMandatoryForJLGLoans(); - final Long productId = this.fromJsonHelper.extractLongNamed("productId", command.parsedJson()); - final LoanProduct loanProduct = this.loanProductRepository.findById(productId) - .orElseThrow(() -> new LoanProductNotFoundException(productId)); - - final Long clientId = this.fromJsonHelper.extractLongNamed("clientId", command.parsedJson()); - if (clientId != null) { - Client client = this.clientRepository.findOneWithNotFoundDetection(clientId); - officeSpecificLoanProductValidation(productId, client.getOffice().getId()); + // Validations (prior assembling) + this.loanApplicationValidator.validateForCreate(command); + // Assembling loan + final Loan loan = this.loanAssembler.assembleFrom(command); + // Validations (further validations which requires the assembling first) + this.loanApplicationValidator.validateForCreate(loan); + // Need to flush to gather loan id + this.loanRepositoryWrapper.saveAndFlush(loan); + // Account number regeneration (need loan id...) + this.loanAssembler.accountNumberGeneration(command, loan); + // Save interest recalculation calendar + if (loan.getLoanProduct().isInterestRecalculationEnabled()) { + createAndPersistCalendarInstanceForInterestRecalculation(loan); } - final Long groupId = this.fromJsonHelper.extractLongNamed("groupId", command.parsedJson()); - if (groupId != null) { - Group group = this.groupRepository.findOneWithNotFoundDetection(groupId); - officeSpecificLoanProductValidation(productId, group.getOffice().getId()); - } - - this.fromApiJsonDeserializer.validateForCreate(command.json(), isMeetingMandatoryForJLGLoans, loanProduct); - - // Validate If the externalId is already registered - final String externalIdStr = this.fromJsonHelper.extractStringNamed("externalId", command.parsedJson()); - 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."); - } - } - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan"); - - if (loanProduct.isUseBorrowerCycle()) { - Integer cycleNumber = 0; - if (clientId != null) { - cycleNumber = this.loanReadPlatformService.retriveLoanCounter(clientId, loanProduct.getId()); - } else if (groupId != null) { - cycleNumber = this.loanReadPlatformService.retriveLoanCounter(groupId, AccountType.GROUP.getValue(), - loanProduct.getId()); - } - this.loanProductCommandFromApiJsonDeserializer.validateMinMaxConstraints(command.parsedJson(), baseDataValidator, - loanProduct, cycleNumber); - } else { - this.loanProductCommandFromApiJsonDeserializer.validateMinMaxConstraints(command.parsedJson(), baseDataValidator, - loanProduct); - } - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - - final Loan newLoanApplication = this.loanAssembler.assembleFrom(command); - final LoanApplicationTerms loanApplicationTerms = this.loanScheduleAssembler.assembleLoanTerms(command.parsedJson()); - loanProductDataValidator.fixedLengthValidations(newLoanApplication.getTransactionProcessingStrategyCode(), loanApplicationTerms, - this.fromJsonHelper.parse(command.json()), baseDataValidator); - - checkForProductMixRestrictions(newLoanApplication); - - validateSubmittedOnDate(newLoanApplication); - - final LoanProductRelatedDetail productRelatedDetail = newLoanApplication.repaymentScheduleDetail(); - - if (loanProduct.getLoanConfigurableAttributes() != null) { - updateProductRelatedDetails(productRelatedDetail, newLoanApplication); - } - - this.fromApiJsonDeserializer.validateLoanTermAndRepaidEveryValues(newLoanApplication.getTermFrequency(), - newLoanApplication.getTermPeriodFrequencyType(), productRelatedDetail.getNumberOfRepayments(), - productRelatedDetail.getRepayEvery(), productRelatedDetail.getRepaymentPeriodFrequencyType().getValue(), - newLoanApplication); - - if (loanProduct.isCanUseForTopup() && clientId != null) { - final Boolean isTopup = command.booleanObjectValueOfParameterNamed(LoanApiConstants.isTopup); - if (null == isTopup) { - newLoanApplication.setIsTopup(false); - } else { - newLoanApplication.setIsTopup(isTopup); - } - - if (newLoanApplication.isTopup()) { - 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(newLoanApplication.getSubmittedOnDate(), disbursalDateOfLoanToClose)) { - throw new GeneralPlatformDomainRuleException( - "error.msg.loan.submitted.date.should.be.after.topup.loan.disbursal.date", - "Submitted date of this loan application " + newLoanApplication.getSubmittedOnDate() - + " should be after the disbursed date of loan to be closed " + disbursalDateOfLoanToClose); - } - if (!loanToClose.getCurrencyCode().equals(newLoanApplication.getCurrencyCode())) { - 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(newLoanApplication.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 " + newLoanApplication.getDisbursementDate() - + " should be after last transaction date of loan to be closed " - + lastUserTransactionOnLoanToClose); - } - BigDecimal loanOutstanding = this.loanReadPlatformService.retrieveLoanPrePaymentTemplate(LoanTransactionType.REPAYMENT, - loanIdToClose, newLoanApplication.getDisbursementDate()).getAmount(); - final BigDecimal firstDisbursalAmount = newLoanApplication.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."); - } - - final LoanTopupDetails topupDetails = new LoanTopupDetails(newLoanApplication, loanIdToClose); - newLoanApplication.setTopupLoanDetails(topupDetails); - } - } - - this.loanRepositoryWrapper.saveAndFlush(newLoanApplication); - - if (loanProduct.isInterestRecalculationEnabled()) { - this.fromApiJsonDeserializer.validateLoanForInterestRecalculation(newLoanApplication); - createAndPersistCalendarInstanceForInterestRecalculation(newLoanApplication); - } - - // loan account number generation - String accountNumber = ""; - GroupLoanIndividualMonitoringAccount glimAccount; - BigDecimal applicationId = BigDecimal.ZERO; - Boolean isLastChildApplication = false; - - if (newLoanApplication.isAccountNumberRequiresAutoGeneration()) { - - final AccountNumberFormat accountNumberFormat = this.accountNumberFormatRepository - .findByAccountType(EntityAccountType.LOAN); - // if application is of GLIM type - if (newLoanApplication.getLoanType() == 4) { - Group group = this.groupRepository.findOneWithNotFoundDetection(groupId); - - // GLIM specific parameters - if (command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("applicationId") != null) { - applicationId = command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("applicationId"); - } - - if (command.booleanObjectValueOfParameterNamed("lastApplication") != null) { - isLastChildApplication = command.booleanPrimitiveValueOfParameterNamed("lastApplication"); - } - - if (command.booleanObjectValueOfParameterNamed("isParentAccount") != null) { - - // empty table check - if (glimRepository.count() != 0) { - // **************Parent-Not an empty - // table******************** - accountNumber = this.accountNumberGenerator.generate(newLoanApplication, accountNumberFormat); - newLoanApplication.updateAccountNo(accountNumber + "1"); - glimAccountInfoWritePlatformService.addGLIMAccountInfo(accountNumber, group, - command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("totalLoan"), Long.valueOf(1), true, - LoanStatus.SUBMITTED_AND_PENDING_APPROVAL.getValue(), applicationId); - newLoanApplication.setGlim(glimRepository.findOneByAccountNumber(accountNumber)); - this.loanRepositoryWrapper.save(newLoanApplication); - - } else { - // ************** Parent-empty - // table******************** - - accountNumber = this.accountNumberGenerator.generate(newLoanApplication, accountNumberFormat); - newLoanApplication.updateAccountNo(accountNumber + "1"); - glimAccountInfoWritePlatformService.addGLIMAccountInfo(accountNumber, group, - command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("totalLoan"), Long.valueOf(1), true, - LoanStatus.SUBMITTED_AND_PENDING_APPROVAL.getValue(), applicationId); - newLoanApplication.setGlim(glimRepository.findOneByAccountNumber(accountNumber)); - this.loanRepositoryWrapper.save(newLoanApplication); - - } - - } else { - - if (glimRepository.count() != 0) { - // Child-Not an empty table - - glimAccount = glimRepository.findOneByIsAcceptingChildAndApplicationId(true, applicationId); - accountNumber = glimAccount.getAccountNumber() + (glimAccount.getChildAccountsCount() + 1); - newLoanApplication.updateAccountNo(accountNumber); - this.glimAccountInfoWritePlatformService.incrementChildAccountCount(glimAccount); - newLoanApplication.setGlim(glimAccount); - this.loanRepositoryWrapper.save(newLoanApplication); - - } else { - // **************Child-empty - // table******************** - // if the glim info is empty set the current account - // as parent - accountNumber = this.accountNumberGenerator.generate(newLoanApplication, accountNumberFormat); - newLoanApplication.updateAccountNo(accountNumber + "1"); - glimAccountInfoWritePlatformService.addGLIMAccountInfo(accountNumber, group, - command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("totalLoan"), Long.valueOf(1), true, - LoanStatus.SUBMITTED_AND_PENDING_APPROVAL.getValue(), applicationId); - newLoanApplication.setGlim(glimRepository.findOneByAccountNumber(accountNumber)); - this.loanRepositoryWrapper.save(newLoanApplication); - - } - - // reset in cases of last child application of glim - - if (isLastChildApplication) { - this.glimAccountInfoWritePlatformService - .resetIsAcceptingChild(glimRepository.findOneByIsAcceptingChildAndApplicationId(true, applicationId)); - } - - } - } else { // for applications other than GLIM - newLoanApplication.updateAccountNo(this.accountNumberGenerator.generate(newLoanApplication, accountNumberFormat)); - this.loanRepositoryWrapper.saveAndFlush(newLoanApplication); - } - } - + // Save note final String submittedOnNote = command.stringValueOfParameterNamed("submittedOnNote"); - if (StringUtils.isNotBlank(submittedOnNote)) { - final Note note = Note.loanNote(newLoanApplication, submittedOnNote); - this.noteRepository.save(note); - } - + createNote(submittedOnNote, loan); // Save calendar instance - final Long calendarId = command.longValueOfParameterNamed("calendarId"); - Calendar calendar = null; - - if (calendarId != null && calendarId != 0) { - calendar = this.calendarRepository.findById(calendarId).orElseThrow(() -> new CalendarNotFoundException(calendarId)); - - final CalendarInstance calendarInstance = new CalendarInstance(calendar, newLoanApplication.getId(), - CalendarEntityType.LOANS.getValue()); - this.calendarInstanceRepository.save(calendarInstance); - } else { - final Integer repaymentFrequencyNthDayType = command.integerValueOfParameterNamed("repaymentFrequencyNthDayType"); - if (loanApplicationTerms.getRepaymentPeriodFrequencyType() == PeriodFrequencyType.MONTHS - && repaymentFrequencyNthDayType != null) { - final String title = "loan_schedule_" + newLoanApplication.getId(); - LocalDate calendarStartDate = loanApplicationTerms.getRepaymentsStartingFromLocalDate(); - if (calendarStartDate == null) { - calendarStartDate = loanApplicationTerms.getExpectedDisbursementDate(); - } - final CalendarFrequencyType calendarFrequencyType = CalendarFrequencyType.MONTHLY; - final Integer frequency = loanApplicationTerms.getRepaymentEvery(); - final Integer repeatsOnDay = loanApplicationTerms.getWeekDayType().getValue(); - final Integer repeatsOnNthDayOfMonth = loanApplicationTerms.getNthDay(); - final Integer calendarEntityType = CalendarEntityType.LOANS.getValue(); - final Calendar loanCalendar = Calendar.createRepeatingCalendar(title, calendarStartDate, - CalendarType.COLLECTION.getValue(), calendarFrequencyType, frequency, repeatsOnDay, repeatsOnNthDayOfMonth); - this.calendarRepository.save(loanCalendar); - final CalendarInstance calendarInstance = CalendarInstance.from(loanCalendar, newLoanApplication.getId(), - calendarEntityType); - this.calendarInstanceRepository.save(calendarInstance); - } - } - + createCalendar(command, loan); // Save linked account information - SavingsAccount savingsAccount; - final boolean backdatedTxnsAllowedTill = false; - AccountAssociations accountAssociations; final Long savingsAccountId = command.longValueOfParameterNamed("linkAccountId"); - if (savingsAccountId != null) { - if (newLoanApplication.getLoanType() == 4) { - - List childSavings = (List) gsimReadPlatformService - .findGSIMAccountsByGSIMId(savingsAccountId); - // List - // childSavings=gsimAccount.getChildGSIMAccounts(); - List gsimClientMembers = new ArrayList(); - Map clientAccountMappings = new HashMap<>(); - for (GroupSavingsIndividualMonitoringAccountData childSaving : childSavings) { - gsimClientMembers.add(childSaving.getClientId()); - clientAccountMappings.put(childSaving.getClientId(), childSaving.getChildAccountId()); - - } - - if (gsimClientMembers.contains(BigDecimal.valueOf(newLoanApplication.getClientId()))) { - savingsAccount = this.savingsAccountAssembler.assembleFrom( - clientAccountMappings.get(BigDecimal.valueOf(newLoanApplication.getClientId())).longValue(), - backdatedTxnsAllowedTill); - - this.fromApiJsonDeserializer.validatelinkedSavingsAccount(savingsAccount, newLoanApplication); - boolean isActive = true; - accountAssociations = AccountAssociations.associateSavingsAccount(newLoanApplication, savingsAccount, - AccountAssociationType.LINKED_ACCOUNT_ASSOCIATION.getValue(), isActive); - this.accountAssociationsRepository.save(accountAssociations); - - } else { - throw new GroupMemberNotFoundInGSIMException(newLoanApplication.getClientId()); - } - } else { - - savingsAccount = this.savingsAccountAssembler.assembleFrom(savingsAccountId, backdatedTxnsAllowedTill); - this.fromApiJsonDeserializer.validatelinkedSavingsAccount(savingsAccount, newLoanApplication); - boolean isActive = true; - accountAssociations = AccountAssociations.associateSavingsAccount(newLoanApplication, savingsAccount, - AccountAssociationType.LINKED_ACCOUNT_ASSOCIATION.getValue(), isActive); - this.accountAssociationsRepository.save(accountAssociations); - - } - } + createSavingsAccountAssociation(savingsAccountId, loan); + // Save related datatable entries if (command.parameterExists(LoanApiConstants.datatables)) { this.entityDatatableChecksWritePlatformService.saveDatatables(StatusEnum.CREATE.getCode().longValue(), - EntityTables.LOAN.getName(), newLoanApplication.getId(), newLoanApplication.productId(), + EntityTables.LOAN.getName(), loan.getId(), loan.productId(), command.arrayOfParameterNamed(LoanApiConstants.datatables)); } - + // TODO: review whether we really need this loanRepositoryWrapper.flush(); - - this.entityDatatableChecksWritePlatformService.runTheCheckForProduct(newLoanApplication.getId(), EntityTables.LOAN.getName(), - StatusEnum.CREATE.getCode().longValue(), EntityTables.LOAN.getForeignKeyColumnNameOnDatatable(), - newLoanApplication.productId()); - - businessEventNotifierService.notifyPostBusinessEvent(new LoanCreatedBusinessEvent(newLoanApplication)); - + // Check mandatory datatable entries were created + this.entityDatatableChecksWritePlatformService.runTheCheckForProduct(loan.getId(), EntityTables.LOAN.getName(), + StatusEnum.CREATE.getCode().longValue(), EntityTables.LOAN.getForeignKeyColumnNameOnDatatable(), loan.productId()); + // Trigger business event + businessEventNotifierService.notifyPostBusinessEvent(new LoanCreatedBusinessEvent(loan)); + // Building response return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // - .withEntityId(newLoanApplication.getId()) // - .withEntityExternalId(newLoanApplication.getExternalId()) // - .withOfficeId(newLoanApplication.getOfficeId()) // - .withClientId(newLoanApplication.getClientId()) // - .withGroupId(newLoanApplication.getGroupId()) // - .withLoanId(newLoanApplication.getId()).withGlimId(newLoanApplication.getGlimId()).build(); + .withEntityId(loan.getId()) // + .withEntityExternalId(loan.getExternalId()) // + .withOfficeId(loan.getOfficeId()) // + .withClientId(loan.getClientId()) // + .withGroupId(loan.getGroupId()) // + .withLoanId(loan.getId()).withGlimId(loan.getGlimId()).build(); } catch (final JpaSystemException | DataIntegrityViolationException dve) { handleDataIntegrityIssues(command, dve.getMostSpecificCause(), dve); return CommandProcessingResult.empty(); @@ -541,80 +193,6 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa } } - public void checkForProductMixRestrictions(final Loan loan) { - - final List activeLoansLoanProductIds; - final Long productId = loan.loanProduct().getId(); - - if (loan.isGroupLoan()) { - activeLoansLoanProductIds = this.loanRepositoryWrapper.findActiveLoansLoanProductIdsByGroup(loan.getGroupId(), - LoanStatus.ACTIVE.getValue()); - } else { - activeLoansLoanProductIds = this.loanRepositoryWrapper.findActiveLoansLoanProductIdsByClient(loan.getClientId(), - LoanStatus.ACTIVE.getValue()); - } - checkForProductMixRestrictions(activeLoansLoanProductIds, productId, loan.loanProduct().getName()); - } - - private void checkForProductMixRestrictions(final List activeLoansLoanProductIds, final Long productId, - final String productName) { - - if (!CollectionUtils.isEmpty(activeLoansLoanProductIds)) { - final Collection restrictedPrdouctsList = this.loanProductReadPlatformService - .retrieveRestrictedProductsForMix(productId); - for (final LoanProductData restrictedProduct : restrictedPrdouctsList) { - if (activeLoansLoanProductIds.contains(restrictedProduct.getId())) { - throw new GeneralPlatformDomainRuleException( - "error.msg.loan.applied.or.to.be.disbursed.can.not.co-exist.with.the.loan.already.active.to.this.client", - "This loan could not be applied/disbursed as the loan and `" + restrictedProduct - + "` are not allowed to co-exist"); - } - } - } - } - - private void updateProductRelatedDetails(LoanProductRelatedDetail productRelatedDetail, Loan loan) { - final Boolean amortization = loan.loanProduct().getLoanConfigurableAttributes().getAmortizationBoolean(); - final Boolean arrearsTolerance = loan.loanProduct().getLoanConfigurableAttributes().getArrearsToleranceBoolean(); - final Boolean graceOnArrearsAging = loan.loanProduct().getLoanConfigurableAttributes().getGraceOnArrearsAgingBoolean(); - final Boolean interestCalcPeriod = loan.loanProduct().getLoanConfigurableAttributes().getInterestCalcPeriodBoolean(); - final Boolean interestMethod = loan.loanProduct().getLoanConfigurableAttributes().getInterestMethodBoolean(); - final Boolean graceOnPrincipalAndInterestPayment = loan.loanProduct().getLoanConfigurableAttributes() - .getGraceOnPrincipalAndInterestPaymentBoolean(); - final Boolean repaymentEvery = loan.loanProduct().getLoanConfigurableAttributes().getRepaymentEveryBoolean(); - final Boolean transactionProcessingStrategy = loan.loanProduct().getLoanConfigurableAttributes() - .getTransactionProcessingStrategyBoolean(); - - if (!amortization) { - productRelatedDetail.setAmortizationMethod(loan.loanProduct().getLoanProductRelatedDetail().getAmortizationMethod()); - } - if (!arrearsTolerance) { - productRelatedDetail.setInArrearsTolerance(loan.loanProduct().getLoanProductRelatedDetail().getArrearsTolerance()); - } - if (!graceOnArrearsAging) { - productRelatedDetail.setGraceOnArrearsAgeing(loan.loanProduct().getLoanProductRelatedDetail().getGraceOnArrearsAgeing()); - } - if (!interestCalcPeriod) { - productRelatedDetail.setInterestCalculationPeriodMethod( - loan.loanProduct().getLoanProductRelatedDetail().getInterestCalculationPeriodMethod()); - } - if (!interestMethod) { - productRelatedDetail.setInterestMethod(loan.loanProduct().getLoanProductRelatedDetail().getInterestMethod()); - } - if (!graceOnPrincipalAndInterestPayment) { - productRelatedDetail.setGraceOnInterestPayment(loan.loanProduct().getLoanProductRelatedDetail().getGraceOnInterestPayment()); - productRelatedDetail.setGraceOnPrincipalPayment(loan.loanProduct().getLoanProductRelatedDetail().getGraceOnPrincipalPayment()); - } - if (!repaymentEvery) { - productRelatedDetail.setRepayEvery(loan.loanProduct().getLoanProductRelatedDetail().getRepayEvery()); - } - if (!transactionProcessingStrategy) { - loan.updateTransactionProcessingStrategy(loan.loanProduct().getTransactionProcessingStrategyCode(), - this.loanRepaymentScheduleTransactionProcessorFactory - .determineProcessor(loan.loanProduct().getTransactionProcessingStrategyCode()).getName()); - } - } - private void createAndPersistCalendarInstanceForInterestRecalculation(final Loan loan) { LocalDate calendarStartDate = loan.getExpectedDisbursedOnLocalDate(); @@ -694,502 +272,42 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa public CommandProcessingResult modifyApplication(final Long loanId, final JsonCommand command) { try { - final Loan existingLoanApplication = retrieveLoanBy(loanId); - if (!existingLoanApplication.isSubmittedAndPendingApproval()) { - throw new LoanApplicationNotInSubmittedAndPendingApprovalStateCannotBeModified(loanId); - } - - final String productIdParamName = "productId"; - LoanProduct newLoanProduct = null; - if (command.isChangeInLongParameterNamed(productIdParamName, existingLoanApplication.loanProduct().getId())) { - final Long productId = command.longValueOfParameterNamed(productIdParamName); - newLoanProduct = this.loanProductRepository.findById(productId) - .orElseThrow(() -> new LoanProductNotFoundException(productId)); - } - - LoanProduct loanProductForValidations = newLoanProduct == null ? existingLoanApplication.loanProduct() : newLoanProduct; - - this.fromApiJsonDeserializer.validateForModify(command.json(), loanProductForValidations, existingLoanApplication); - - checkClientOrGroupActive(existingLoanApplication); - - final Set existingCharges = existingLoanApplication.getActiveCharges(); - Map chargesMap = new HashMap<>(); - for (LoanCharge charge : existingCharges) { - LoanChargeData chargeData = new LoanChargeData(charge.getId(), charge.getDueLocalDate(), charge.amountOrPercentage()); - chargesMap.put(charge.getId(), chargeData); - } - List disbursementDetails = this.loanUtilService - .fetchDisbursementData(command.parsedJson().getAsJsonObject()); - - /** - * Stores all charges which are passed in during modify loan application - **/ - final Set possiblyModifedLoanCharges = this.loanChargeAssembler.fromParsedJson(command.parsedJson(), - disbursementDetails); - /** Boolean determines if any charge has been modified **/ - boolean isChargeModified = false; - - Set newTrancheChages = this.loanChargeAssembler.getNewLoanTrancheCharges(command.parsedJson()); - for (Charge charge : newTrancheChages) { - existingLoanApplication.addTrancheLoanCharge(charge); - } - - /** - * If there are any charges already present, which are now not passed in as a part of the request, deem the - * charges as modified - **/ - if (!possiblyModifedLoanCharges.isEmpty()) { - if (!possiblyModifedLoanCharges.containsAll(existingCharges)) { - isChargeModified = true; - } - } - - /** - * If any new charges are added or values of existing charges are modified - **/ - for (LoanCharge loanCharge : possiblyModifedLoanCharges) { - if (loanCharge.getId() == null) { - isChargeModified = true; - } else { - LoanChargeData chargeData = chargesMap.get(loanCharge.getId()); - if (loanCharge.amountOrPercentage().compareTo(chargeData.getAmountOrPercentage()) != 0 - || (loanCharge.isSpecifiedDueDate() && !loanCharge.getDueLocalDate().equals(chargeData.getDueDate()))) { - isChargeModified = true; - } - } - } - - Set possiblyModifedLoanCollateralItems = null; - - if (command.parameterExists("loanType")) { - final String loanTypeStr = command.stringValueOfParameterNamed("loanType"); - final AccountType loanType = AccountType.fromName(loanTypeStr); - - if (!StringUtils.isBlank(loanTypeStr) && loanType.isIndividualAccount()) { - possiblyModifedLoanCollateralItems = this.loanCollateralAssembler.fromParsedJson(command.parsedJson()); - } - } - - final Map changes = existingLoanApplication.loanApplicationModification(command, possiblyModifedLoanCharges, - possiblyModifedLoanCollateralItems, this.aprCalculator, isChargeModified, loanProductForValidations); - - if (changes.containsKey("expectedDisbursementDate")) { - this.loanAssembler.validateExpectedDisbursementForHolidayAndNonWorkingDay(existingLoanApplication); - } - - final String clientIdParamName = "clientId"; - if (changes.containsKey(clientIdParamName)) { - final Long clientId = command.longValueOfParameterNamed(clientIdParamName); - final Client client = this.clientRepository.findOneWithNotFoundDetection(clientId); - if (client.isNotActive()) { - throw new ClientNotActiveException(clientId); - } - - existingLoanApplication.updateClient(client); - } - - final String groupIdParamName = "groupId"; - if (changes.containsKey(groupIdParamName)) { - final Long groupId = command.longValueOfParameterNamed(groupIdParamName); - final Group group = this.groupRepository.findOneWithNotFoundDetection(groupId); - if (group.isNotActive()) { - throw new GroupNotActiveException(groupId); - } - - existingLoanApplication.updateGroup(group); - } - - if (newLoanProduct != null) { - existingLoanApplication.updateLoanProduct(newLoanProduct); - if (!changes.containsKey("interestRateFrequencyType")) { - existingLoanApplication.updateInterestRateFrequencyType(); - } - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan"); - if (newLoanProduct.isUseBorrowerCycle()) { - final Long clientId = this.fromJsonHelper.extractLongNamed("clientId", command.parsedJson()); - final Long groupId = this.fromJsonHelper.extractLongNamed("groupId", command.parsedJson()); - Integer cycleNumber = 0; - if (clientId != null) { - cycleNumber = this.loanReadPlatformService.retriveLoanCounter(clientId, newLoanProduct.getId()); - } else if (groupId != null) { - cycleNumber = this.loanReadPlatformService.retriveLoanCounter(groupId, AccountType.GROUP.getValue(), - newLoanProduct.getId()); - } - this.loanProductCommandFromApiJsonDeserializer.validateMinMaxConstraints(command.parsedJson(), baseDataValidator, - newLoanProduct, cycleNumber); - } else { - this.loanProductCommandFromApiJsonDeserializer.validateMinMaxConstraints(command.parsedJson(), baseDataValidator, - newLoanProduct); - } - if (newLoanProduct.isLinkedToFloatingInterestRate()) { - existingLoanApplication.getLoanProductRelatedDetail().updateForFloatingInterestRates(); - } else { - existingLoanApplication.setInterestRateDifferential(null); - existingLoanApplication.setIsFloatingInterestRate(null); - } - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - existingLoanApplication.updateIsInterestRecalculationEnabled(); - validateSubmittedOnDate(existingLoanApplication); - - final LoanProductRelatedDetail productRelatedDetail = existingLoanApplication.repaymentScheduleDetail(); - if (existingLoanApplication.loanProduct().getLoanConfigurableAttributes() != null) { - updateProductRelatedDetails(productRelatedDetail, existingLoanApplication); - } - - if (existingLoanApplication.getLoanProduct().isCanUseForTopup() && existingLoanApplication.getClientId() != null) { - final Boolean isTopup = command.booleanObjectValueOfParameterNamed(LoanApiConstants.isTopup); - if (command.isChangeInBooleanParameterNamed(LoanApiConstants.isTopup, existingLoanApplication.isTopup())) { - existingLoanApplication.setIsTopup(isTopup); - changes.put(LoanApiConstants.isTopup, isTopup); - } - - if (existingLoanApplication.isTopup()) { - final Long loanIdToClose = command.longValueOfParameterNamed(LoanApiConstants.loanIdToClose); - LoanTopupDetails existingLoanTopupDetails = existingLoanApplication.getTopupLoanDetails(); - if (existingLoanTopupDetails == null - || (existingLoanTopupDetails != null && !existingLoanTopupDetails.getLoanIdToClose().equals(loanIdToClose)) - || changes.containsKey("submittedOnDate") || changes.containsKey("expectedDisbursementDate") - || changes.containsKey("principal") || changes.containsKey(LoanApiConstants.disbursementDataParameterName)) { - Long existingLoanIdToClose = null; - if (existingLoanTopupDetails != null) { - existingLoanIdToClose = existingLoanTopupDetails.getLoanIdToClose(); - } - final Loan loanToClose = this.loanRepositoryWrapper.findNonClosedLoanThatBelongsToClient(loanIdToClose, - existingLoanApplication.getClientId()); - 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(existingLoanApplication.getSubmittedOnDate(), disbursalDateOfLoanToClose)) { - throw new GeneralPlatformDomainRuleException( - "error.msg.loan.submitted.date.should.be.after.topup.loan.disbursal.date", - "Submitted date of this loan application " + existingLoanApplication.getSubmittedOnDate() - + " should be after the disbursed date of loan to be closed " + disbursalDateOfLoanToClose); - } - if (!loanToClose.getCurrencyCode().equals(existingLoanApplication.getCurrencyCode())) { - 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(existingLoanApplication.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 " + existingLoanApplication.getDisbursementDate() - + " should be after last transaction date of loan to be closed " - + lastUserTransactionOnLoanToClose); - } - BigDecimal loanOutstanding = this.loanReadPlatformService.retrieveLoanPrePaymentTemplate( - LoanTransactionType.REPAYMENT, loanIdToClose, existingLoanApplication.getDisbursementDate()).getAmount(); - final BigDecimal firstDisbursalAmount = existingLoanApplication.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."); - } - - if (!existingLoanIdToClose.equals(loanIdToClose)) { - final LoanTopupDetails topupDetails = new LoanTopupDetails(existingLoanApplication, loanIdToClose); - existingLoanApplication.setTopupLoanDetails(topupDetails); - changes.put(LoanApiConstants.loanIdToClose, loanIdToClose); - } - } - } else { - existingLoanApplication.setTopupLoanDetails(null); - } - } else { - if (existingLoanApplication.isTopup()) { - existingLoanApplication.setIsTopup(false); - existingLoanApplication.setTopupLoanDetails(null); - changes.put(LoanApiConstants.isTopup, false); - } - } - - final String fundIdParamName = "fundId"; - if (changes.containsKey(fundIdParamName)) { - final Long fundId = command.longValueOfParameterNamed(fundIdParamName); - final Fund fund = this.loanAssembler.findFundByIdIfProvided(fundId); - - existingLoanApplication.updateFund(fund); - } - - final String loanPurposeIdParamName = "loanPurposeId"; - if (changes.containsKey(loanPurposeIdParamName)) { - final Long loanPurposeId = command.longValueOfParameterNamed(loanPurposeIdParamName); - final CodeValue loanPurpose = this.loanAssembler.findCodeValueByIdIfProvided(loanPurposeId); - existingLoanApplication.updateLoanPurpose(loanPurpose); - } - - final String loanOfficerIdParamName = "loanOfficerId"; - if (changes.containsKey(loanOfficerIdParamName)) { - final Long loanOfficerId = command.longValueOfParameterNamed(loanOfficerIdParamName); - final Staff newValue = this.loanAssembler.findLoanOfficerByIdIfProvided(loanOfficerId); - existingLoanApplication.updateLoanOfficerOnLoanApplication(newValue); - } - - final String strategyCodeParamName = "transactionProcessingStrategyCode"; - if (changes.containsKey(strategyCodeParamName)) { - final String strategy = command.stringValueOfParameterNamed(strategyCodeParamName); - - final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory - .determineProcessor(strategy); - validateTransactionProcessingStrategy(existingLoanApplication.getLoanProductRelatedDetail().getLoanScheduleType(), - loanRepaymentScheduleTransactionProcessor.getCode()); - - existingLoanApplication.updateTransactionProcessingStrategy(strategy, loanRepaymentScheduleTransactionProcessor.getName()); - } - - /** - * TODO: Allow other loan types if needed. - */ - if (command.parameterExists("loanType")) { - final String loanTypeStr = command.stringValueOfParameterNamed("loanType"); - final AccountType loanType = AccountType.fromName(loanTypeStr); - - if (!StringUtils.isBlank(loanTypeStr) && loanType.isIndividualAccount()) { - final String collateralParamName = "collateral"; - if (changes.containsKey(collateralParamName)) { - existingLoanApplication.updateLoanCollateral(possiblyModifedLoanCollateralItems); - } - } - } - - final String chargesParamName = "charges"; - if (changes.containsKey(chargesParamName)) { - existingLoanApplication.updateLoanCharges(possiblyModifedLoanCharges); - } - - // update installment level delinquency - if (command.isChangeInBooleanParameterNamed(LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY, - existingLoanApplication.isEnableInstallmentLevelDelinquency())) { - final Boolean enableInstallmentLevelDelinquency = command - .booleanObjectValueOfParameterNamed(LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY); - existingLoanApplication.updateEnableInstallmentLevelDelinquency(enableInstallmentLevelDelinquency); - } - - if (changes.containsKey("recalculateLoanSchedule")) { - changes.remove("recalculateLoanSchedule"); - - final JsonElement parsedQuery = this.fromJsonHelper.parse(command.json()); - final JsonQuery query = JsonQuery.from(command.json(), parsedQuery, this.fromJsonHelper); - - final LoanScheduleModel loanSchedule = this.calculationPlatformService.calculateLoanSchedule(query, false); - existingLoanApplication.updateLoanSchedule(loanSchedule); - existingLoanApplication.recalculateAllCharges(); - } - - // Changes to modify loan rates. - if (command.hasParameter(LoanProductConstants.RATES_PARAM_NAME)) { - existingLoanApplication.updateLoanRates(rateAssembler.fromParsedJson(command.parsedJson())); - } - - this.fromApiJsonDeserializer.validateLoanTermAndRepaidEveryValues(existingLoanApplication.getTermFrequency(), - existingLoanApplication.getTermPeriodFrequencyType(), productRelatedDetail.getNumberOfRepayments(), - productRelatedDetail.getRepayEvery(), productRelatedDetail.getRepaymentPeriodFrequencyType().getValue(), - existingLoanApplication); - - saveAndFlushLoanWithDataIntegrityViolationChecks(existingLoanApplication); - + Loan loan = retrieveLoanBy(loanId); + // Validations (prior assembling) + this.loanApplicationValidator.validateForModify(command, loan); + // Assembling loan + Map changes = this.loanAssembler.updateFrom(command, loan); + // Validations (further validations which requires the assembling first) + this.loanApplicationValidator.validateForModify(loan); + // TODO: check whether this is needed! + loan = loanRepository.saveAndFlush(loan); + // Save note final String submittedOnNote = command.stringValueOfParameterNamed("submittedOnNote"); - if (StringUtils.isNotBlank(submittedOnNote)) { - final Note note = Note.loanNote(existingLoanApplication, submittedOnNote); - this.noteRepository.save(note); - } - + createNote(submittedOnNote, loan); + // Modify calendar instance final Long calendarId = command.longValueOfParameterNamed("calendarId"); - Calendar calendar = null; - if (calendarId != null && calendarId != 0) { - calendar = this.calendarRepository.findById(calendarId).orElseThrow(() -> new CalendarNotFoundException(calendarId)); - } - - final List ciList = (List) this.calendarInstanceRepository - .findByEntityIdAndEntityTypeId(loanId, CalendarEntityType.LOANS.getValue()); - if (calendar != null) { - - // For loans, allow to attach only one calendar instance per - // loan - if (ciList != null && !ciList.isEmpty()) { - final CalendarInstance calendarInstance = ciList.get(0); - final boolean isCalendarAssociatedWithEntity = this.calendarReadPlatformService.isCalendarAssociatedWithEntity( - calendarInstance.getEntityId(), calendarInstance.getCalendar().getId(), - CalendarEntityType.LOANS.getValue().longValue()); - if (isCalendarAssociatedWithEntity && calendarId == null) { - this.calendarRepository.delete(calendarInstance.getCalendar()); - } - if (!calendarInstance.getCalendar().getId().equals(calendar.getId())) { - calendarInstance.updateCalendar(calendar); - this.calendarInstanceRepository.saveAndFlush(calendarInstance); - } - } else { - // attaching new calendar - final CalendarInstance calendarInstance = new CalendarInstance(calendar, existingLoanApplication.getId(), - CalendarEntityType.LOANS.getValue()); - this.calendarInstanceRepository.save(calendarInstance); - } - - } else { - if (ciList != null && !ciList.isEmpty()) { - final CalendarInstance existingCalendarInstance = ciList.get(0); - final boolean isCalendarAssociatedWithEntity = this.calendarReadPlatformService.isCalendarAssociatedWithEntity( - existingCalendarInstance.getEntityId(), existingCalendarInstance.getCalendar().getId(), - CalendarEntityType.GROUPS.getValue().longValue()); - if (isCalendarAssociatedWithEntity) { - this.calendarInstanceRepository.delete(existingCalendarInstance); - } - } - if (changes.containsKey("repaymentFrequencyNthDayType") || changes.containsKey("repaymentFrequencyDayOfWeekType")) { - if (changes.get("repaymentFrequencyNthDayType") == null) { - if (ciList != null && !ciList.isEmpty()) { - final CalendarInstance calendarInstance = ciList.get(0); - final boolean isCalendarAssociatedWithEntity = this.calendarReadPlatformService.isCalendarAssociatedWithEntity( - calendarInstance.getEntityId(), calendarInstance.getCalendar().getId(), - CalendarEntityType.LOANS.getValue().longValue()); - if (isCalendarAssociatedWithEntity) { - this.calendarInstanceRepository.delete(calendarInstance); - this.calendarRepository.delete(calendarInstance.getCalendar()); - } - } - } else { - Integer repaymentFrequencyTypeInt = command.integerValueOfParameterNamed("repaymentFrequencyType"); - if (repaymentFrequencyTypeInt != null) { - if (PeriodFrequencyType.fromInt(repaymentFrequencyTypeInt) == PeriodFrequencyType.MONTHS) { - final String title = "loan_schedule_" + existingLoanApplication.getId(); - final Integer typeId = CalendarType.COLLECTION.getValue(); - final CalendarFrequencyType repaymentFrequencyType = CalendarFrequencyType.MONTHLY; - final Integer interval = command.integerValueOfParameterNamed("repaymentEvery"); - LocalDate startDate = command.localDateValueOfParameterNamed("repaymentsStartingFromDate"); - if (startDate == null) { - startDate = command.localDateValueOfParameterNamed("expectedDisbursementDate"); - } - final Calendar newCalendar = Calendar.createRepeatingCalendar(title, startDate, typeId, - repaymentFrequencyType, interval, (Integer) changes.get("repaymentFrequencyDayOfWeekType"), - (Integer) changes.get("repaymentFrequencyNthDayType")); - if (ciList != null && !ciList.isEmpty()) { - final CalendarInstance calendarInstance = ciList.get(0); - final boolean isCalendarAssociatedWithEntity = this.calendarReadPlatformService - .isCalendarAssociatedWithEntity(calendarInstance.getEntityId(), - calendarInstance.getCalendar().getId(), - CalendarEntityType.LOANS.getValue().longValue()); - if (isCalendarAssociatedWithEntity) { - final Calendar existingCalendar = calendarInstance.getCalendar(); - if (existingCalendar != null) { - String existingRecurrence = existingCalendar.getRecurrence(); - if (!existingRecurrence.equals(newCalendar.getRecurrence())) { - existingCalendar.setRecurrence(newCalendar.getRecurrence()); - this.calendarRepository.save(existingCalendar); - } - } - } - } else { - this.calendarRepository.save(newCalendar); - final Integer calendarEntityType = CalendarEntityType.LOANS.getValue(); - final CalendarInstance calendarInstance = new CalendarInstance(newCalendar, - existingLoanApplication.getId(), calendarEntityType); - this.calendarInstanceRepository.save(calendarInstance); - } - } - } - } - } - } - + modifyCalendar(loanId, calendarId, loan, changes); // Save linked account information - final String linkAccountIdParamName = "linkAccountId"; - final boolean backdatedTxnsAllowedTill = false; - final Long savingsAccountId = command.longValueOfParameterNamed(linkAccountIdParamName); - AccountAssociations accountAssociations = this.accountAssociationsRepository.findByLoanIdAndType(loanId, - AccountAssociationType.LINKED_ACCOUNT_ASSOCIATION.getValue()); - boolean isLinkedAccPresent = false; - if (savingsAccountId == null) { - if (accountAssociations != null) { - if (this.fromJsonHelper.parameterExists(linkAccountIdParamName, command.parsedJson())) { - this.accountAssociationsRepository.delete(accountAssociations); - changes.put(linkAccountIdParamName, null); - } else { - isLinkedAccPresent = true; - } - } - } else { - isLinkedAccPresent = true; - boolean isModified = false; - if (accountAssociations == null) { - isModified = true; - } else { - final SavingsAccount savingsAccount = accountAssociations.linkedSavingsAccount(); - if (savingsAccount == null || !savingsAccount.getId().equals(savingsAccountId)) { - isModified = true; - } - } - if (isModified) { - final SavingsAccount savingsAccount = this.savingsAccountAssembler.assembleFrom(savingsAccountId, - backdatedTxnsAllowedTill); - this.fromApiJsonDeserializer.validatelinkedSavingsAccount(savingsAccount, existingLoanApplication); - if (accountAssociations == null) { - boolean isActive = true; - accountAssociations = AccountAssociations.associateSavingsAccount(existingLoanApplication, savingsAccount, - AccountAssociationType.LINKED_ACCOUNT_ASSOCIATION.getValue(), isActive); - } else { - accountAssociations.updateLinkedSavingsAccount(savingsAccount); - } - changes.put(linkAccountIdParamName, savingsAccountId); - this.accountAssociationsRepository.save(accountAssociations); - } - } - - if (!isLinkedAccPresent) { - final Set charges = existingLoanApplication.getActiveCharges(); - for (final LoanCharge loanCharge : charges) { - if (loanCharge.getChargePaymentMode().isPaymentModeAccountTransfer()) { - final String errorMessage = "one of the charges requires linked savings account for payment"; - throw new LinkedAccountRequiredException("loanCharge", errorMessage); - } - } - } - - if ((command.longValueOfParameterNamed(productIdParamName) != null) - || (command.longValueOfParameterNamed(clientIdParamName) != null) - || (command.longValueOfParameterNamed(groupIdParamName) != null)) { - Long OfficeId = null; - if (existingLoanApplication.getClient() != null) { - OfficeId = existingLoanApplication.getClient().getOffice().getId(); - } else if (existingLoanApplication.getGroup() != null) { - OfficeId = existingLoanApplication.getGroup().getOffice().getId(); - } - officeSpecificLoanProductValidation(existingLoanApplication.getLoanProduct().getId(), OfficeId); - } + modifyLinkedAccount(command, changes, loan); // updating loan interest recalculation details throwing null // pointer exception after saveAndFlush // http://stackoverflow.com/questions/17151757/hibernate-cascade-update-gives-null-pointer/17334374#17334374 - this.loanRepositoryWrapper.saveAndFlush(existingLoanApplication); - - if (productRelatedDetail.isInterestRecalculationEnabled()) { - this.fromApiJsonDeserializer.validateLoanForInterestRecalculation(existingLoanApplication); - if (changes.containsKey(LoanProductConstants.IS_INTEREST_RECALCULATION_ENABLED_PARAMETER_NAME)) { - createAndPersistCalendarInstanceForInterestRecalculation(existingLoanApplication); - - } - + // TODO: check whether this is needed! + this.loanRepositoryWrapper.saveAndFlush(loan); + // Save interest recalculation calendar + if (loan.getLoanProductRelatedDetail().isInterestRecalculationEnabled() + && changes.containsKey(LoanProductConstants.IS_INTEREST_RECALCULATION_ENABLED_PARAMETER_NAME)) { + createAndPersistCalendarInstanceForInterestRecalculation(loan); } return new CommandProcessingResultBuilder() // .withEntityId(loanId) // - .withEntityExternalId(existingLoanApplication.getExternalId()) // - .withOfficeId(existingLoanApplication.getOfficeId()) // - .withClientId(existingLoanApplication.getClientId()) // - .withGroupId(existingLoanApplication.getGroupId()) // - .withLoanId(existingLoanApplication.getId()) // + .withEntityExternalId(loan.getExternalId()) // + .withOfficeId(loan.getOfficeId()) // + .withClientId(loan.getClientId()) // + .withGroupId(loan.getGroupId()) // + .withLoanId(loan.getId()) // .with(changes).build(); } catch (final JpaSystemException | DataIntegrityViolationException dve) { handleDataIntegrityIssues(command, dve.getMostSpecificCause(), dve); @@ -1201,24 +319,160 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa } } + private void modifyLinkedAccount(JsonCommand command, Map changes, Loan loan) { + final Long savingsAccountId = command.longValueOfParameterNamed(LoanApiConstants.linkAccountIdParameterName); + final boolean linkedAccountWasProvided = command.parameterExists(LoanApiConstants.linkAccountIdParameterName); + // Only process if something was provided + if (linkedAccountWasProvided) { + AccountAssociations accountAssociations = this.accountAssociationsRepository.findByLoanIdAndType(loan.getId(), + AccountAssociationType.LINKED_ACCOUNT_ASSOCIATION.getValue()); + // Explicit null value was provided as linked account id, so we shall remove the association + if (savingsAccountId == null) { + removeLinkedAccountAssociation(accountAssociations, changes); + } else { + final SavingsAccount savingsAccount = this.savingsAccountRepository.findOneWithNotFoundDetection(savingsAccountId); + // If there was no previous + if (accountAssociations == null) { + createLinkedAccountAssociation(loan, savingsAccount, changes); + // When the previous one was linking to a different account + } else if (!accountAssociations.linkedSavingsAccount().getId().equals(savingsAccountId)) { + updateLinkedAccountAssociation(accountAssociations, savingsAccount, changes); + } + } + } + } + + private void updateLinkedAccountAssociation(AccountAssociations accountAssociations, SavingsAccount savingsAccount, + Map changes) { + accountAssociations.updateLinkedSavingsAccount(savingsAccount); + this.accountAssociationsRepository.save(accountAssociations); + changes.put(LoanApiConstants.linkAccountIdParameterName, savingsAccount.getId()); + } + + private void removeLinkedAccountAssociation(final AccountAssociations accountAssociations, final Map changes) { + if (accountAssociations != null) { + this.accountAssociationsRepository.delete(accountAssociations); + changes.put(LoanApiConstants.linkAccountIdParameterName, null); + } + } + + private void createLinkedAccountAssociation(final Loan loan, final SavingsAccount savingsAccount, final Map changes) { + boolean isActive = true; + this.accountAssociationsRepository.save(AccountAssociations.associateSavingsAccount(loan, savingsAccount, + AccountAssociationType.LINKED_ACCOUNT_ASSOCIATION.getValue(), isActive)); + changes.put(LoanApiConstants.linkAccountIdParameterName, savingsAccount.getId()); + } + + private void modifyCalendar(Long loanId, Long calendarId, Loan loan, Map changes) { + Calendar calendar = null; + if (calendarId != null && calendarId != 0) { + calendar = this.calendarRepository.findById(calendarId).orElseThrow(() -> new CalendarNotFoundException(calendarId)); + } + + final List ciList = (List) this.calendarInstanceRepository.findByEntityIdAndEntityTypeId(loanId, + CalendarEntityType.LOANS.getValue()); + if (calendar != null) { + // For loans, allow to attach only one calendar instance per + // loan + if (ciList != null && !ciList.isEmpty()) { + final CalendarInstance calendarInstance = ciList.get(0); + final boolean isCalendarAssociatedWithEntity = this.calendarReadPlatformService.isCalendarAssociatedWithEntity( + calendarInstance.getEntityId(), calendarInstance.getCalendar().getId(), + CalendarEntityType.LOANS.getValue().longValue()); + if (isCalendarAssociatedWithEntity && calendarId == null) { + this.calendarRepository.delete(calendarInstance.getCalendar()); + } + if (!calendarInstance.getCalendar().getId().equals(calendar.getId())) { + calendarInstance.updateCalendar(calendar); + this.calendarInstanceRepository.saveAndFlush(calendarInstance); + } + } else { + // attaching new calendar + final CalendarInstance calendarInstance = new CalendarInstance(calendar, loan.getId(), CalendarEntityType.LOANS.getValue()); + this.calendarInstanceRepository.save(calendarInstance); + } + + } else { + if (ciList != null && !ciList.isEmpty()) { + final CalendarInstance existingCalendarInstance = ciList.get(0); + final boolean isCalendarAssociatedWithEntity = this.calendarReadPlatformService.isCalendarAssociatedWithEntity( + existingCalendarInstance.getEntityId(), existingCalendarInstance.getCalendar().getId(), + CalendarEntityType.GROUPS.getValue().longValue()); + if (isCalendarAssociatedWithEntity) { + this.calendarInstanceRepository.delete(existingCalendarInstance); + } + } + if (changes.containsKey("repaymentFrequencyNthDayType") || changes.containsKey("repaymentFrequencyDayOfWeekType")) { + if (changes.get("repaymentFrequencyNthDayType") == null) { + if (ciList != null && !ciList.isEmpty()) { + final CalendarInstance calendarInstance = ciList.get(0); + final boolean isCalendarAssociatedWithEntity = this.calendarReadPlatformService.isCalendarAssociatedWithEntity( + calendarInstance.getEntityId(), calendarInstance.getCalendar().getId(), + CalendarEntityType.LOANS.getValue().longValue()); + if (isCalendarAssociatedWithEntity) { + this.calendarInstanceRepository.delete(calendarInstance); + this.calendarRepository.delete(calendarInstance.getCalendar()); + } + } + } else { + PeriodFrequencyType repaymentFrequencyType = loan.repaymentScheduleDetail().getRepaymentPeriodFrequencyType(); + if (repaymentFrequencyType == PeriodFrequencyType.MONTHS) { + final String title = "loan_schedule_" + loan.getId(); + final Integer typeId = CalendarType.COLLECTION.getValue(); + final CalendarFrequencyType calendarFrequencyType = CalendarFrequencyType.MONTHLY; + final Integer interval = loan.repaymentScheduleDetail().getRepayEvery(); + LocalDate startDate = loan.getExpectedFirstRepaymentOnDate(); + if (startDate == null) { + startDate = loan.getExpectedDisbursedOnLocalDate(); + } + final Calendar newCalendar = Calendar.createRepeatingCalendar(title, startDate, typeId, calendarFrequencyType, + interval, (Integer) changes.get("repaymentFrequencyDayOfWeekType"), + (Integer) changes.get("repaymentFrequencyNthDayType")); + if (ciList != null && !ciList.isEmpty()) { + final CalendarInstance calendarInstance = ciList.get(0); + final boolean isCalendarAssociatedWithEntity = this.calendarReadPlatformService.isCalendarAssociatedWithEntity( + calendarInstance.getEntityId(), calendarInstance.getCalendar().getId(), + CalendarEntityType.LOANS.getValue().longValue()); + if (isCalendarAssociatedWithEntity) { + final Calendar existingCalendar = calendarInstance.getCalendar(); + if (existingCalendar != null) { + String existingRecurrence = existingCalendar.getRecurrence(); + if (!existingRecurrence.equals(newCalendar.getRecurrence())) { + existingCalendar.setRecurrence(newCalendar.getRecurrence()); + this.calendarRepository.save(existingCalendar); + } + } + } + } else { + this.calendarRepository.save(newCalendar); + final Integer calendarEntityType = CalendarEntityType.LOANS.getValue(); + final CalendarInstance calendarInstance = new CalendarInstance(newCalendar, loan.getId(), calendarEntityType); + this.calendarInstanceRepository.save(calendarInstance); + } + } + } + } + } + } + /* * Guaranteed to throw an exception no matter what the data integrity issue is. */ private void handleDataIntegrityIssues(final JsonCommand command, final Throwable realCause, final Exception dve) { if (realCause.getMessage().contains("loan_account_no_UNIQUE") || (realCause.getCause() != null && realCause.getCause().getMessage().contains("loan_account_no_UNIQUE"))) { - final String accountNo = command.stringValueOfParameterNamed("accountNo"); throw new PlatformDataIntegrityException("error.msg.loan.duplicate.accountNo", "Loan with accountNo `" + accountNo + "` already exists", "accountNo", accountNo); } else if (realCause.getMessage().contains("loan_externalid_UNIQUE") - || (realCause.getCause() != null && realCause.getCause().getMessage().contains("loan_externalid_UNIQUE"))) { + || (realCause.getCause() != null && realCause.getCause().getMessage().contains("loan_externalid_UNIQUE")) + || realCause.getMessage().toLowerCase().contains("external_id_unique")) { final String externalId = command.stringValueOfParameterNamed("externalId"); throw new PlatformDataIntegrityException("error.msg.loan.duplicate.externalId", "Loan with externalId `" + externalId + "` already exists", "externalId", externalId); } - log.error("Error occured.", dve); + log.error("Error occurred.", dve); throw ErrorHandler.getMappable(dve, "error.msg.unknown.data.integrity.issue", "Unknown data integrity issue with resource."); } @@ -1270,7 +524,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa final List dataValidationErrors = new ArrayList<>(); final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan"); final BigDecimal principal = this.fromJsonHelper.extractBigDecimalWithLocaleNamed("approvedLoanAmount", element); - fromApiJsonDeserializer.validateLoanMultiDisbursementDate(element, baseDataValidator, expectedDisbursementDate, principal); + loanApplicationValidator.validateLoanMultiDisbursementDate(element, baseDataValidator, expectedDisbursementDate, principal); if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } @@ -1331,7 +585,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa this.loanApplicationTransitionApiJsonValidator.validateApproval(command.json()); - final Loan loan = retrieveLoanBy(loanId); + Loan loan = retrieveLoanBy(loanId); final JsonArray disbursementDataArray = command.arrayOfParameterNamed(LoanApiConstants.disbursementDataParameterName); @@ -1410,7 +664,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa loan.adjustNetDisbursalAmount(netDisbursalAmount); } - saveAndFlushLoanWithDataIntegrityViolationChecks(loan); + loan = loanRepository.saveAndFlush(loan); final String noteText = command.stringValueOfParameterNamed("note"); if (StringUtils.isNotBlank(noteText)) { @@ -1469,9 +723,9 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa @Override public CommandProcessingResult undoApplicationApproval(final Long loanId, final JsonCommand command) { - this.fromApiJsonDeserializer.validateForUndo(command.json()); + this.loanApplicationValidator.validateForUndo(command.json()); - final Loan loan = retrieveLoanBy(loanId); + Loan loan = retrieveLoanBy(loanId); checkClientOrGroupActive(loan); final Map changes = loan.undoApproval(defaultLoanLifecycleStateMachine); @@ -1489,13 +743,10 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa loan.adjustNetDisbursalAmount(loan.getProposedPrincipal()); - saveAndFlushLoanWithDataIntegrityViolationChecks(loan); + loan = loanRepository.saveAndFlush(loan); final String noteText = command.stringValueOfParameterNamed("note"); - if (StringUtils.isNotBlank(noteText)) { - final Note note = Note.loanNote(loan, noteText); - this.noteRepository.save(note); - } + createNote(noteText, loan); businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoApprovalBusinessEvent(loan)); } @@ -1562,10 +813,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa this.loanRepositoryWrapper.saveAndFlush(loan); final String noteText = command.stringValueOfParameterNamed("note"); - if (StringUtils.isNotBlank(noteText)) { - final Note note = Note.loanNote(loan, noteText); - this.noteRepository.save(note); - } + createNote(noteText, loan); } businessEventNotifierService.notifyPostBusinessEvent(new LoanRejectedBusinessEvent(loan)); return new CommandProcessingResultBuilder() // @@ -1598,7 +846,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa defaultLoanLifecycleStateMachine); // Release attached collaterals - if (AccountType.fromInt(loan.getLoanType()).isIndividualAccount()) { + if (loan.getLoanType().isIndividualAccount()) { Set loanCollateralManagements = loan.getLoanCollateralManagements(); for (LoanCollateralManagement loanCollateralManagement : loanCollateralManagements) { ClientCollateralManagement clientCollateralManagement = loanCollateralManagement.getClientCollateralManagement(); @@ -1614,10 +862,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa this.loanRepositoryWrapper.saveAndFlush(loan); final String noteText = command.stringValueOfParameterNamed("note"); - if (StringUtils.isNotBlank(noteText)) { - final Note note = Note.loanNote(loan, noteText); - this.noteRepository.save(note); - } + createNote(noteText, loan); } return new CommandProcessingResultBuilder() // @@ -1638,32 +883,6 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa return loan; } - private void validateSubmittedOnDate(final Loan loan) { - final LocalDate startDate = loan.loanProduct().getStartDate(); - final LocalDate closeDate = loan.loanProduct().getCloseDate(); - final LocalDate expectedFirstRepaymentOnDate = loan.getExpectedFirstRepaymentOnDate(); - final LocalDate submittedOnDate = loan.getSubmittedOnDate(); - - String defaultUserMessage = ""; - if (DateUtils.isBefore(submittedOnDate, startDate)) { - defaultUserMessage = "submittedOnDate cannot be before the loan product startDate."; - throw new LoanApplicationDateException("submitted.on.date.cannot.be.before.the.loan.product.start.date", defaultUserMessage, - submittedOnDate.toString(), startDate.toString()); - } - - if (closeDate != null && DateUtils.isAfter(submittedOnDate, closeDate)) { - defaultUserMessage = "submittedOnDate cannot be after the loan product closeDate."; - throw new LoanApplicationDateException("submitted.on.date.cannot.be.after.the.loan.product.close.date", defaultUserMessage, - submittedOnDate.toString(), closeDate.toString()); - } - - if (expectedFirstRepaymentOnDate != null && DateUtils.isAfter(submittedOnDate, expectedFirstRepaymentOnDate)) { - defaultUserMessage = "submittedOnDate cannot be after the loans expectedFirstRepaymentOnDate."; - throw new LoanApplicationDateException("submitted.on.date.cannot.be.after.the.loan.expected.first.repayment.date", - defaultUserMessage, submittedOnDate.toString(), expectedFirstRepaymentOnDate.toString()); - } - } - private void checkClientOrGroupActive(final Loan loan) { final Client client = loan.client(); if (client != null) { @@ -1679,23 +898,6 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa } } - private void saveAndFlushLoanWithDataIntegrityViolationChecks(final Loan loan) { - try { - this.loanRepositoryWrapper.saveAndFlush(loan); - } catch (final JpaSystemException | DataIntegrityViolationException e) { - final Throwable realCause = e.getCause(); - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.application"); - if (realCause.getMessage().toLowerCase().contains("external_id_unique")) { - baseDataValidator.reset().parameter("externalId").failWithCode("value.must.be.unique"); - } - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", - dataValidationErrors, e); - } - } - } - private AppUser getAppUserIfPresent() { AppUser user = null; if (this.context != null) { @@ -1704,37 +906,75 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa return user; } - private void officeSpecificLoanProductValidation(final Long productId, final Long officeId) { - final GlobalConfigurationProperty restrictToUserOfficeProperty = this.globalConfigurationRepository - .findOneByNameWithNotFoundDetection(FineractEntityAccessConstants.GLOBAL_CONFIG_FOR_OFFICE_SPECIFIC_PRODUCTS); - if (restrictToUserOfficeProperty.isEnabled()) { - FineractEntityRelation fineractEntityRelation = fineractEntityRelationRepository - .findOneByCodeName(FineractEntityAccessType.OFFICE_ACCESS_TO_LOAN_PRODUCTS.getStr()); - FineractEntityToEntityMapping officeToLoanProductMappingList = this.entityMappingRepository - .findListByProductId(fineractEntityRelation, productId, officeId); - if (officeToLoanProductMappingList == null) { - throw new NotOfficeSpecificProductException(productId, officeId); + private void createSavingsAccountAssociation(Long savingsAccountId, Loan loan) { + if (savingsAccountId != null) { + SavingsAccount savingsAccount; + AccountAssociations accountAssociations; + if (loan.getLoanType().isGLIMAccount()) { + List childSavings = (List) gsimReadPlatformService + .findGSIMAccountsByGSIMId(savingsAccountId); + List gsimClientMembers = new ArrayList<>(); + Map clientAccountMappings = new HashMap<>(); + for (GroupSavingsIndividualMonitoringAccountData childSaving : childSavings) { + gsimClientMembers.add(childSaving.getClientId()); + clientAccountMappings.put(childSaving.getClientId(), childSaving.getChildAccountId()); + + } + + if (gsimClientMembers.contains(BigDecimal.valueOf(loan.getClientId()))) { + savingsAccount = this.savingsAccountRepository + .findOneWithNotFoundDetection(clientAccountMappings.get(BigDecimal.valueOf(loan.getClientId())).longValue()); + } else { + throw new GroupMemberNotFoundInGSIMException(loan.getClientId()); + } + } else { + savingsAccount = this.savingsAccountRepository.findOneWithNotFoundDetection(savingsAccountId); } + boolean isActive = true; + accountAssociations = AccountAssociations.associateSavingsAccount(loan, savingsAccount, + AccountAssociationType.LINKED_ACCOUNT_ASSOCIATION.getValue(), isActive); + this.accountAssociationsRepository.save(accountAssociations); } } - private void validateTransactionProcessingStrategy(final LoanScheduleType loanScheduleType, final String loanRepaymentStrategy) { - // PROGRESSIVE: Repayment strategy MUST be only "advanced payment allocation" - if (loanScheduleType.equals(LoanScheduleType.PROGRESSIVE)) { - if (!loanRepaymentStrategy.equals(LoanProductConstants.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)) { - throw new GeneralPlatformDomainRuleException( - "error.msg.loan.repayment.strategy.can.not.be.different.than.advanced.payment.allocation", - "Loan repayment strategy can not be different than Advanced Payment Allocation"); - } - // CUMULATIVE: Repayment strategy CANNOT be "advanced payment allocation" - } else if (loanScheduleType.equals(LoanScheduleType.CUMULATIVE)) { - if (loanRepaymentStrategy.equals(LoanProductConstants.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)) { - throw new GeneralPlatformDomainRuleException( - "error.msg.loan.repayment.strategy.can.not.be.equal.to.advanced.payment.allocation", - "Loan repayment strategy can not be equal to Advanced Payment Allocation"); + private void createCalendar(JsonCommand command, Loan loan) { + final Long calendarId = command.longValueOfParameterNamed("calendarId"); + Calendar calendar; + + if (calendarId != null && calendarId != 0) { + calendar = this.calendarRepository.findById(calendarId).orElseThrow(() -> new CalendarNotFoundException(calendarId)); + final CalendarInstance calendarInstance = new CalendarInstance(calendar, loan.getId(), CalendarEntityType.LOANS.getValue()); + this.calendarInstanceRepository.save(calendarInstance); + } else { + // TODO: Would be nice to avoid recreating the loan application terms once again + final LoanApplicationTerms loanApplicationTerms = this.loanScheduleAssembler.assembleLoanTerms(command.parsedJson()); + if (loanApplicationTerms.getRepaymentPeriodFrequencyType() == PeriodFrequencyType.MONTHS + && loanApplicationTerms.getNthDay() != null) { + final String title = "loan_schedule_" + loan.getId(); + LocalDate calendarStartDate = loanApplicationTerms.getRepaymentsStartingFromLocalDate(); + if (calendarStartDate == null) { + calendarStartDate = loanApplicationTerms.getExpectedDisbursementDate(); + } + final CalendarFrequencyType calendarFrequencyType = CalendarFrequencyType.MONTHLY; + final Integer frequency = loanApplicationTerms.getRepaymentEvery(); + final Integer repeatsOnDay = loanApplicationTerms.getWeekDayType().getValue(); + final Integer repeatsOnNthDayOfMonth = loanApplicationTerms.getNthDay(); + final Integer calendarEntityType = CalendarEntityType.LOANS.getValue(); + final Calendar loanCalendar = Calendar.createRepeatingCalendar(title, calendarStartDate, CalendarType.COLLECTION.getValue(), + calendarFrequencyType, frequency, repeatsOnDay, repeatsOnNthDayOfMonth); + this.calendarRepository.save(loanCalendar); + final CalendarInstance calendarInstance = CalendarInstance.from(loanCalendar, loan.getId(), calendarEntityType); + this.calendarInstanceRepository.save(calendarInstance); } } } + private void createNote(String submittedOnNote, Loan newLoanApplication) { + if (StringUtils.isNotBlank(submittedOnNote)) { + final Note note = Note.loanNote(newLoanApplication, submittedOnNote); + this.noteRepository.save(note); + } + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java index e8dd42def..6bf8744dd 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java @@ -18,29 +18,35 @@ */ package org.apache.fineract.portfolio.loanaccount.service; +import com.google.gson.JsonArray; import com.google.gson.JsonElement; import java.math.BigDecimal; import java.time.LocalDate; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.infrastructure.accountnumberformat.domain.AccountNumberFormat; +import org.apache.fineract.infrastructure.accountnumberformat.domain.AccountNumberFormatRepositoryWrapper; +import org.apache.fineract.infrastructure.accountnumberformat.domain.EntityAccountType; import org.apache.fineract.infrastructure.codes.domain.CodeValue; import org.apache.fineract.infrastructure.codes.domain.CodeValueRepositoryWrapper; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; +import org.apache.fineract.infrastructure.configuration.service.TemporaryConfigurationServiceContainer; import org.apache.fineract.infrastructure.core.api.JsonCommand; -import org.apache.fineract.infrastructure.core.data.EnumOptionData; +import org.apache.fineract.infrastructure.core.api.JsonQuery; import org.apache.fineract.infrastructure.core.domain.ExternalId; -import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.organisation.holiday.domain.Holiday; import org.apache.fineract.organisation.holiday.domain.HolidayRepository; import org.apache.fineract.organisation.holiday.domain.HolidayStatusType; -import org.apache.fineract.organisation.monetary.exception.InvalidCurrencyException; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.staff.domain.Staff; import org.apache.fineract.organisation.staff.domain.StaffRepository; import org.apache.fineract.organisation.staff.exception.StaffNotFoundException; @@ -48,21 +54,20 @@ import org.apache.fineract.organisation.staff.exception.StaffRoleException; import org.apache.fineract.organisation.workingdays.domain.WorkingDays; import org.apache.fineract.organisation.workingdays.domain.WorkingDaysRepositoryWrapper; import org.apache.fineract.portfolio.accountdetails.domain.AccountType; -import org.apache.fineract.portfolio.accountdetails.service.AccountEnumerations; +import org.apache.fineract.portfolio.charge.domain.Charge; +import org.apache.fineract.portfolio.client.domain.AccountNumberGenerator; import org.apache.fineract.portfolio.client.domain.Client; import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper; -import org.apache.fineract.portfolio.client.exception.ClientNotActiveException; -import org.apache.fineract.portfolio.collateralmanagement.domain.CollateralManagementDomain; import org.apache.fineract.portfolio.collateralmanagement.service.LoanCollateralAssembler; import org.apache.fineract.portfolio.fund.domain.Fund; import org.apache.fineract.portfolio.fund.domain.FundRepository; import org.apache.fineract.portfolio.fund.exception.FundNotFoundException; import org.apache.fineract.portfolio.group.domain.Group; -import org.apache.fineract.portfolio.group.domain.GroupRepository; -import org.apache.fineract.portfolio.group.exception.ClientNotInGroupException; -import org.apache.fineract.portfolio.group.exception.GroupNotActiveException; -import org.apache.fineract.portfolio.group.exception.GroupNotFoundException; +import org.apache.fineract.portfolio.group.domain.GroupRepositoryWrapper; import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; +import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData; +import org.apache.fineract.portfolio.loanaccount.domain.GLIMAccountInfoRepository; +import org.apache.fineract.portfolio.loanaccount.domain.GroupLoanIndividualMonitoringAccount; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagement; @@ -72,21 +77,24 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachin import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; +import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; import org.apache.fineract.portfolio.loanaccount.domain.LoanSummaryWrapper; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTopupDetails; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.exception.ExceedingTrancheCountException; -import org.apache.fineract.portfolio.loanaccount.exception.InvalidAmountOfCollaterals; import org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementDataNotAllowedException; import org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementDataRequiredException; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; -import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleAssembler; +import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleCalculationPlatformService; +import org.apache.fineract.portfolio.loanaccount.mapper.LoanChargeMapper; +import org.apache.fineract.portfolio.loanaccount.mapper.LoanCollateralManagementMapper; import org.apache.fineract.portfolio.loanproduct.LoanProductConstants; import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository; -import org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException; import org.apache.fineract.portfolio.loanproduct.exception.LoanProductNotFoundException; import org.apache.fineract.portfolio.rate.domain.Rate; import org.apache.fineract.portfolio.rate.service.RateAssembler; @@ -98,7 +106,7 @@ public class LoanAssembler { private final LoanRepositoryWrapper loanRepository; private final LoanProductRepository loanProductRepository; private final ClientRepositoryWrapper clientRepository; - private final GroupRepository groupRepository; + private final GroupRepositoryWrapper groupRepository; private final FundRepository fundRepository; private final StaffRepository staffRepository; private final CodeValueRepositoryWrapper codeValueRepository; @@ -110,10 +118,18 @@ public class LoanAssembler { private final HolidayRepository holidayRepository; private final ConfigurationDomainService configurationDomainService; private final WorkingDaysRepositoryWrapper workingDaysRepository; - private final LoanUtilService loanUtilService; private final RateAssembler rateAssembler; private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine; private final ExternalIdFactory externalIdFactory; + private final AccountNumberFormatRepositoryWrapper accountNumberFormatRepository; + private final GLIMAccountInfoRepository glimRepository; + private final AccountNumberGenerator accountNumberGenerator; + private final GLIMAccountInfoWritePlatformService glimAccountInfoWritePlatformService; + private final LoanCollateralAssembler loanCollateralAssembler; + private final LoanScheduleCalculationPlatformService calculationPlatformService; + private final LoanDisbursementDetailsAssembler loanDisbursementDetailsAssembler; + private final LoanChargeMapper loanChargeMapper; + private final LoanCollateralManagementMapper loanCollateralManagementMapper; public Loan assembleFrom(final Long accountId) { final Loan loanAccount = this.loanRepository.findOneWithNotFoundDetection(accountId, true); @@ -133,20 +149,10 @@ public class LoanAssembler { final Long clientId = this.fromApiJsonHelper.extractLongNamed("clientId", element); final Long groupId = this.fromApiJsonHelper.extractLongNamed("groupId", element); - - return assembleApplication(element, clientId, groupId); - } - - private Loan assembleApplication(final JsonElement element, final Long clientId, final Long groupId) { - final String accountNo = this.fromApiJsonHelper.extractStringNamed("accountNo", element); final Long productId = this.fromApiJsonHelper.extractLongNamed("productId", element); final Long fundId = this.fromApiJsonHelper.extractLongNamed("fundId", element); final Long loanOfficerId = this.fromApiJsonHelper.extractLongNamed("loanOfficerId", element); - final String transactionProcessingStrategyCode = this.fromApiJsonHelper.extractStringNamed("transactionProcessingStrategyCode", - element); - final String transactionProcessingStrategyName = this.loanRepaymentScheduleTransactionProcessorFactory - .determineProcessor(transactionProcessingStrategyCode).getName(); final Long loanPurposeId = this.fromApiJsonHelper.extractLongNamed("loanPurposeId", element); final Boolean syncDisbursementWithMeeting = this.fromApiJsonHelper.extractBooleanNamed("syncDisbursementWithMeeting", element); final Boolean createStandingInstructionAtDisbursement = this.fromApiJsonHelper @@ -154,8 +160,13 @@ public class LoanAssembler { final LoanProduct loanProduct = this.loanProductRepository.findById(productId) .orElseThrow(() -> new LoanProductNotFoundException(productId)); - final BigDecimal amount = this.fromApiJsonHelper - .extractBigDecimalWithLocaleNamed(LoanApiConstants.disbursementPrincipalParameterName, element); + final Boolean allowOverridingTransactionProcessingStrategy = loanProduct.getLoanConfigurableAttributes() + .getTransactionProcessingStrategyBoolean(); + final String transactionProcessingStrategyCode = allowOverridingTransactionProcessingStrategy + ? this.fromApiJsonHelper.extractStringNamed("transactionProcessingStrategyCode", element) + : loanProduct.getTransactionProcessingStrategyCode(); + final LoanRepaymentScheduleTransactionProcessor transactionProcessingStrategy = this.loanRepaymentScheduleTransactionProcessorFactory + .determineProcessor(transactionProcessingStrategyCode); final Fund fund = findFundByIdIfProvided(fundId); final Staff loanOfficer = findLoanOfficerByIdIfProvided(loanOfficerId); CodeValue loanPurpose = null; @@ -165,76 +176,26 @@ public class LoanAssembler { List disbursementDetails = new ArrayList<>(); BigDecimal fixedEmiAmount = null; if (loanProduct.isMultiDisburseLoan() || loanProduct.isCanDefineInstallmentAmount()) { - fixedEmiAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.emiAmountParameterName, element); + fixedEmiAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.fixedEmiAmountParameterName, element); } BigDecimal maxOutstandingLoanBalance = null; if (loanProduct.isMultiDisburseLoan()) { final Locale locale = this.fromApiJsonHelper.extractLocaleParameter(element.getAsJsonObject()); maxOutstandingLoanBalance = this.fromApiJsonHelper.extractBigDecimalNamed(LoanApiConstants.maxOutstandingBalanceParameterName, element, locale); - - disbursementDetails = this.loanUtilService.fetchDisbursementData(element.getAsJsonObject()); - if (loanProduct.isDisallowExpectedDisbursements()) { - if (!disbursementDetails.isEmpty()) { - final String errorMessage = "For this loan product, disbursement details are not allowed"; - throw new MultiDisbursementDataNotAllowedException(LoanApiConstants.disbursementDataParameterName, errorMessage); - } - } else { - if (disbursementDetails.isEmpty()) { - final String errorMessage = "For this loan product, disbursement details must be provided"; - throw new MultiDisbursementDataRequiredException(LoanApiConstants.disbursementDataParameterName, errorMessage); - } - } - - if (disbursementDetails.size() > loanProduct.maxTrancheCount()) { - final String errorMessage = "Number of tranche shouldn't be greter than " + loanProduct.maxTrancheCount(); - throw new ExceedingTrancheCountException(LoanApiConstants.disbursementDataParameterName, errorMessage, - loanProduct.maxTrancheCount(), disbursementDetails.size()); - } + disbursementDetails = this.loanDisbursementDetailsAssembler.fetchDisbursementData(element.getAsJsonObject()); } - final String loanTypeParameterName = "loanType"; - final String loanTypeStr = this.fromApiJsonHelper.extractStringNamed(loanTypeParameterName, element); - final EnumOptionData loanType = AccountEnumerations.loanType(loanTypeStr); + final String loanTypeStr = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.loanTypeParameterName, element); Set collateral = new HashSet<>(); - if (!StringUtils.isBlank(loanTypeStr)) { - final AccountType loanAccountType = AccountType.fromName(loanTypeStr); - - if (loanAccountType.isIndividualAccount()) { - collateral = this.collateralAssembler.fromParsedJson(element); - - if (collateral.size() > 0) { - BigDecimal totalValue = BigDecimal.ZERO; - for (LoanCollateralManagement collateralManagement : collateral) { - final CollateralManagementDomain collateralManagementDomain = collateralManagement.getClientCollateralManagement() - .getCollaterals(); - BigDecimal totalCollateral = collateralManagement.getQuantity().multiply(collateralManagementDomain.getBasePrice()) - .multiply(collateralManagementDomain.getPctToBase()).divide(BigDecimal.valueOf(100)); - totalValue = totalValue.add(totalCollateral); - } - - if (amount.compareTo(totalValue) > 0) { - throw new InvalidAmountOfCollaterals(totalValue); - } - } - } + final AccountType loanAccountType = AccountType.fromName(loanTypeStr); + if (loanAccountType.isIndividualAccount()) { + collateral = this.collateralAssembler.fromParsedJson(element); } final Set loanCharges = this.loanChargeAssembler.fromParsedJson(element, disbursementDetails); - for (final LoanCharge loanCharge : loanCharges) { - if (!loanProduct.hasCurrencyCodeOf(loanCharge.currencyCode())) { - final String errorMessage = "Charge and Loan must have the same currency."; - throw new InvalidCurrencyException("loanCharge", "attach.to.loan", errorMessage); - } - if (loanCharge.getChargePaymentMode().isPaymentModeAccountTransfer()) { - final Long savingsAccountId = this.fromApiJsonHelper.extractLongNamed("linkAccountId", element); - if (savingsAccountId == null) { - final String errorMessage = "one of the charges requires linked savings account for payment"; - throw new LinkedAccountRequiredException("loanCharge", errorMessage); - } - } - } + BigDecimal fixedPrincipalPercentagePerInstallment = fromApiJsonHelper .extractBigDecimalWithLocaleNamed(LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName, element); @@ -245,111 +206,166 @@ public class LoanAssembler { // Here we add Rates to LoanApplication final List rates = this.rateAssembler.fromParsedJson(element); - final LoanProductRelatedDetail loanProductRelatedDetail = this.loanScheduleAssembler.assembleLoanProductRelatedDetail(element); + final LoanProductRelatedDetail loanProductRelatedDetail = this.loanScheduleAssembler.assembleLoanProductRelatedDetail(element, + loanProduct); final BigDecimal interestRateDifferential = this.fromApiJsonHelper .extractBigDecimalWithLocaleNamed(LoanApiConstants.interestRateDifferentialParameterName, element); final Boolean isFloatingInterestRate = this.fromApiJsonHelper .extractBooleanNamed(LoanApiConstants.isFloatingInterestRateParameterName, element); - // PROGRESSIVE: Repayment strategy MUST be only "advanced payment allocation" - final LoanScheduleType loanScheduleType = loanProduct.getLoanProductRelatedDetail().getLoanScheduleType(); - if (loanScheduleType.equals(LoanScheduleType.PROGRESSIVE)) { - if (!transactionProcessingStrategyCode.equals(LoanProductConstants.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)) { - throw new GeneralPlatformDomainRuleException( - "error.msg.loan.repayment.strategy.can.not.be.different.than.advanced.payment.allocation", - "Loan repayment strategy can not be different than Advanced Payment Allocation"); - } - // CUMULATIVE: Repayment strategy CANNOT be "advanced payment allocation" - } else if (loanScheduleType.equals(LoanScheduleType.CUMULATIVE)) { - if (transactionProcessingStrategyCode.equals(LoanProductConstants.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)) { - throw new GeneralPlatformDomainRuleException( - "error.msg.loan.repayment.strategy.can.not.be.equal.to.advanced.payment.allocation", - "Loan repayment strategy can not be equal to Advanced Payment Allocation"); - } - } - if (clientId != null) { client = this.clientRepository.findOneWithNotFoundDetection(clientId); - if (client.isNotActive()) { - throw new ClientNotActiveException(clientId); - } } if (groupId != null) { - group = this.groupRepository.findById(groupId).orElseThrow(() -> new GroupNotFoundException(groupId)); - if (group.isNotActive()) { - throw new GroupNotActiveException(groupId); - } - } - - if (client != null && group != null) { - - if (!group.hasClientAsMember(client)) { - throw new ClientNotInGroupException(clientId, groupId); - } - - loanApplication = Loan.newIndividualLoanApplicationFromGroup(accountNo, client, group, loanType.getId().intValue(), loanProduct, - fund, loanOfficer, loanPurpose, transactionProcessingStrategyCode, loanProductRelatedDetail, loanCharges, null, - syncDisbursementWithMeeting, fixedEmiAmount, disbursementDetails, maxOutstandingLoanBalance, - createStandingInstructionAtDisbursement, isFloatingInterestRate, interestRateDifferential, rates, - fixedPrincipalPercentagePerInstallment); - } else if (group != null) { - loanApplication = Loan.newGroupLoanApplication(accountNo, group, loanType.getId().intValue(), loanProduct, fund, loanOfficer, - loanPurpose, transactionProcessingStrategyCode, loanProductRelatedDetail, loanCharges, null, - syncDisbursementWithMeeting, fixedEmiAmount, disbursementDetails, maxOutstandingLoanBalance, - createStandingInstructionAtDisbursement, isFloatingInterestRate, interestRateDifferential, rates, - fixedPrincipalPercentagePerInstallment); - } else if (client != null) { - loanApplication = Loan.newIndividualLoanApplication(accountNo, client, loanType.getId().intValue(), loanProduct, fund, - loanOfficer, loanPurpose, transactionProcessingStrategyCode, loanProductRelatedDetail, loanCharges, collateral, - fixedEmiAmount, disbursementDetails, maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, - isFloatingInterestRate, interestRateDifferential, rates, fixedPrincipalPercentagePerInstallment); - } else { - loanApplication = null; + group = this.groupRepository.findOneWithNotFoundDetection(groupId); } final String externalIdStr = this.fromApiJsonHelper.extractStringNamed("externalId", element); ExternalId externalId = externalIdFactory.create(externalIdStr); final LocalDate submittedOnDate = this.fromApiJsonHelper.extractLocalDateNamed("submittedOnDate", element); - if (loanApplication == null) { - throw new IllegalStateException("No loan application exists for either a client or group (or both)."); - } - loanApplication.updateTransactionProcessingStrategy(transactionProcessingStrategyCode, transactionProcessingStrategyName); - copyAdvancedPaymentRulesIfApplicable(transactionProcessingStrategyCode, loanProduct, loanApplication); - loanApplication.setHelpers(defaultLoanLifecycleStateMachine, this.loanSummaryWrapper, - this.loanRepaymentScheduleTransactionProcessorFactory); - - if (loanProduct.isMultiDisburseLoan()) { - for (final LoanDisbursementDetails loanDisbursementDetails : loanApplication.getDisbursementDetails()) { - loanDisbursementDetails.updateLoan(loanApplication); - } - } - - final Boolean isEnableInstallmentLevelDelinquency = this.fromApiJsonHelper + Boolean isEnableInstallmentLevelDelinquency = this.fromApiJsonHelper .extractBooleanNamed(LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY, element); - if (isEnableInstallmentLevelDelinquency != null) { - loanApplication.updateEnableInstallmentLevelDelinquency(isEnableInstallmentLevelDelinquency); - } else { - loanApplication.updateEnableInstallmentLevelDelinquency(loanProduct.isEnableInstallmentLevelDelinquency()); + if (isEnableInstallmentLevelDelinquency == null) { + isEnableInstallmentLevelDelinquency = loanProduct.isEnableInstallmentLevelDelinquency(); } final LoanApplicationTerms loanApplicationTerms = this.loanScheduleAssembler.assembleLoanTerms(element); final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled(); - final List holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loanApplication.getOfficeId(), + Long officeId = client != null ? client.getOffice().getId() : group.getOffice().getId(); + final List holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(officeId, loanApplicationTerms.getExpectedDisbursementDate(), HolidayStatusType.ACTIVE.getValue()); final WorkingDays workingDays = this.workingDaysRepository.findOne(); - final boolean allowTransactionsOnNonWorkingDay = this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled(); - final boolean allowTransactionsOnHoliday = this.configurationDomainService.allowTransactionsOnHolidayEnabled(); final LoanScheduleModel loanScheduleModel = this.loanScheduleAssembler.assembleLoanScheduleFrom(loanApplicationTerms, isHolidayEnabled, holidays, workingDays, element, disbursementDetails); - loanApplication.loanApplicationSubmittal(loanScheduleModel, loanApplicationTerms, defaultLoanLifecycleStateMachine, submittedOnDate, - externalId, allowTransactionsOnHoliday, holidays, workingDays, allowTransactionsOnNonWorkingDay); + + if (client != null && group != null) { + loanApplication = Loan.newIndividualLoanApplicationFromGroup(accountNo, client, group, loanAccountType, loanProduct, fund, + loanOfficer, loanPurpose, transactionProcessingStrategy, loanProductRelatedDetail, loanCharges, + syncDisbursementWithMeeting, fixedEmiAmount, disbursementDetails, maxOutstandingLoanBalance, + createStandingInstructionAtDisbursement, isFloatingInterestRate, interestRateDifferential, rates, + fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms, loanScheduleModel, + isEnableInstallmentLevelDelinquency, submittedOnDate); + } else if (group != null) { + loanApplication = Loan.newGroupLoanApplication(accountNo, group, loanAccountType, loanProduct, fund, loanOfficer, loanPurpose, + transactionProcessingStrategy, loanProductRelatedDetail, loanCharges, syncDisbursementWithMeeting, fixedEmiAmount, + disbursementDetails, maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, isFloatingInterestRate, + interestRateDifferential, rates, fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms, + loanScheduleModel, isEnableInstallmentLevelDelinquency, submittedOnDate); + } else if (client != null) { + loanApplication = Loan.newIndividualLoanApplication(accountNo, client, loanAccountType, loanProduct, fund, loanOfficer, + loanPurpose, transactionProcessingStrategy, loanProductRelatedDetail, loanCharges, collateral, fixedEmiAmount, + disbursementDetails, maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, isFloatingInterestRate, + interestRateDifferential, rates, fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms, + loanScheduleModel, isEnableInstallmentLevelDelinquency, submittedOnDate); + } else { + throw new IllegalStateException("No loan application exists for either a client or group (or both)."); + } + + copyAdvancedPaymentRulesIfApplicable(transactionProcessingStrategyCode, loanProduct, loanApplication); + loanApplication.setHelpers(defaultLoanLifecycleStateMachine, this.loanSummaryWrapper, + this.loanRepaymentScheduleTransactionProcessorFactory); + // TODO: review + loanApplication.recalculateAllCharges(); + topUpLoanConfiguration(element, loanApplication); return loanApplication; } + // TODO: Review... it might be better somewhere else and rethink due to the account number generation logic is + // intertwined with GLIM logic + public void accountNumberGeneration(JsonCommand command, Loan loan) { + if (loan.isAccountNumberRequiresAutoGeneration()) { + JsonElement element = command.parsedJson(); + final AccountNumberFormat accountNumberFormat = this.accountNumberFormatRepository.findByAccountType(EntityAccountType.LOAN); + // TODO: It is really weird to set GLIM info only if account number was not provided + // if application is of GLIM type + if (loan.getLoanType().isGLIMAccount()) { + Group group = loan.getGroup(); + String accountNumber = ""; + BigDecimal applicationId = BigDecimal.ZERO; + Boolean isLastChildApplication = false; + // GLIM specific parameters + final Locale locale = this.fromApiJsonHelper.extractLocaleParameter(element.getAsJsonObject()); + BigDecimal applicationIdFromParam = this.fromApiJsonHelper.extractBigDecimalNamed("applicationId", element, locale); + BigDecimal totalLoan = this.fromApiJsonHelper.extractBigDecimalNamed("totalLoan", element, locale); + if (applicationIdFromParam != null) { + applicationId = applicationIdFromParam; + } + + Boolean isLastChildApplicationFromParam = this.fromApiJsonHelper.extractBooleanNamed("lastApplication", element); + if (isLastChildApplicationFromParam != null) { + isLastChildApplication = isLastChildApplicationFromParam; + } + + if (this.fromApiJsonHelper.extractBooleanNamed("isParentAccount", element) != null) { + // empty table check + // TODO: This count here is weird... and seems parent-empty and parent not empty looks the same + if (glimRepository.count() != 0) { + // **************Parent-Not an empty + // table******************** + createAndSetGLIMAccount(totalLoan, loan, accountNumberFormat, group, applicationId); + } else { + // ************** Parent-empty + // table******************** + createAndSetGLIMAccount(totalLoan, loan, accountNumberFormat, group, applicationId); + } + } else { + // TODO: This count here is weird... + if (glimRepository.count() != 0) { + // Child-Not an empty table + GroupLoanIndividualMonitoringAccount glimAccount = glimRepository.findOneByIsAcceptingChildAndApplicationId(true, + applicationId); + accountNumber = glimAccount.getAccountNumber() + (glimAccount.getChildAccountsCount() + 1); + loan.updateAccountNo(accountNumber); + this.glimAccountInfoWritePlatformService.incrementChildAccountCount(glimAccount); + loan.setGlim(glimAccount); + } else { + // **************Child-empty + // table******************** + // if the glim info is empty set the current account + // as parent + createAndSetGLIMAccount(totalLoan, loan, accountNumberFormat, group, applicationId); + } + // reset in cases of last child application of glim + if (isLastChildApplication) { + this.glimAccountInfoWritePlatformService + .resetIsAcceptingChild(glimRepository.findOneByIsAcceptingChildAndApplicationId(true, applicationId)); + } + } + } else { // for applications other than GLIM + loan.updateAccountNo(this.accountNumberGenerator.generate(loan, accountNumberFormat)); + } + } + } + + private void createAndSetGLIMAccount(BigDecimal totalLoan, Loan loan, AccountNumberFormat accountNumberFormat, Group group, + BigDecimal applicationId) { + String accountNumber; + accountNumber = this.accountNumberGenerator.generate(loan, accountNumberFormat); + loan.updateAccountNo(accountNumber + "1"); + GroupLoanIndividualMonitoringAccount glimAccount = glimAccountInfoWritePlatformService.createGLIMAccount(accountNumber, group, + totalLoan, 1L, true, LoanStatus.SUBMITTED_AND_PENDING_APPROVAL.getValue(), applicationId); + loan.setGlim(glimAccount); + } + + private void topUpLoanConfiguration(JsonElement element, Loan loan) { + if (loan.getLoanProduct().isCanUseForTopup() && loan.getClientId() != null) { + final Boolean isTopUp = this.fromApiJsonHelper.extractBooleanNamed(LoanApiConstants.isTopup, element); + if (null == isTopUp) { + loan.setIsTopup(false); + } else { + loan.setIsTopup(isTopUp); + } + if (loan.isTopup()) { + final Long loanIdToClose = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.loanIdToClose, element); + loan.setTopupLoanDetails(new LoanTopupDetails(loan, loanIdToClose)); + } + } + } + public CodeValue findCodeValueByIdIfProvided(final Long codeValueId) { CodeValue codeValue = null; if (codeValueId != null) { @@ -377,18 +393,6 @@ public class LoanAssembler { return staff; } - public void validateExpectedDisbursementForHolidayAndNonWorkingDay(final Loan loanApplication) { - - final boolean allowTransactionsOnHoliday = this.configurationDomainService.allowTransactionsOnHolidayEnabled(); - final List holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loanApplication.getOfficeId(), - loanApplication.getExpectedDisbursedOnLocalDate(), HolidayStatusType.ACTIVE.getValue()); - final WorkingDays workingDays = this.workingDaysRepository.findOne(); - final boolean allowTransactionsOnNonWorkingDay = this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled(); - - loanApplication.validateExpectedDisbursementForHolidayAndNonWorkingDay(workingDays, allowTransactionsOnHoliday, holidays, - allowTransactionsOnNonWorkingDay); - } - private void copyAdvancedPaymentRulesIfApplicable(String transactionProcessingStrategyCode, LoanProduct loanProduct, Loan loanApplication) { if (transactionProcessingStrategyCode.equals(AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)) { @@ -398,11 +402,435 @@ public class LoanAssembler { .toList(); loanApplication.setPaymentAllocationRules(loanPaymentAllocationRules); - if (loanProduct.getCreditAllocationRules() != null && loanProduct.getCreditAllocationRules().size() > 0) { + if (loanProduct.getCreditAllocationRules() != null && !loanProduct.getCreditAllocationRules().isEmpty()) { List loanCreditAllocationRules = loanProduct.getCreditAllocationRules().stream() .map(r -> new LoanCreditAllocationRule(loanApplication, r.getTransactionType(), r.getAllocationTypes())).toList(); loanApplication.setCreditAllocationRules(loanCreditAllocationRules); } } } + + public Map updateFrom(JsonCommand command, Loan loan) { + final Map changes = new HashMap<>(); + LoanProduct loanProduct; + + final String productIdParamName = "productId"; + final Long productId = command.longValueOfParameterNamed(productIdParamName); + if (productId == null || productId.equals(loan.getLoanProduct().getId())) { + loanProduct = loan.getLoanProduct(); + } else { + loanProduct = this.loanProductRepository.findById(productId).orElseThrow(() -> new LoanProductNotFoundException(productId)); + } + + final Set existingCharges = loan.getActiveCharges(); + Map chargesMap = new HashMap<>(); + for (LoanCharge charge : existingCharges) { + LoanChargeData chargeData = new LoanChargeData(charge.getId(), charge.getDueLocalDate(), charge.amountOrPercentage()); + chargesMap.put(charge.getId(), chargeData); + } + List disbursementDetails = this.loanDisbursementDetailsAssembler + .fetchDisbursementData(command.parsedJson().getAsJsonObject()); + + /** + * Stores all charges which are passed in during modify loan application + **/ + final Set possiblyModifiedLoanCharges = this.loanChargeAssembler.fromParsedJson(command.parsedJson(), + disbursementDetails); + /** Boolean determines if any charge has been modified **/ + boolean isChargeModified = false; + + Set newTrancheCharges = this.loanChargeAssembler.getNewLoanTrancheCharges(command.parsedJson()); + for (Charge charge : newTrancheCharges) { + loan.addTrancheLoanCharge(charge); + } + + /** + * If there are any charges already present, which are now not passed in as a part of the request, deem the + * charges as modified + **/ + if (!possiblyModifiedLoanCharges.isEmpty()) { + if (!possiblyModifiedLoanCharges.containsAll(existingCharges)) { + isChargeModified = true; + } + } + + /** + * If any new charges are added or values of existing charges are modified + **/ + for (LoanCharge loanCharge : possiblyModifiedLoanCharges) { + if (loanCharge.getId() == null) { + isChargeModified = true; + } else { + LoanChargeData chargeData = chargesMap.get(loanCharge.getId()); + if (loanCharge.amountOrPercentage().compareTo(chargeData.getAmountOrPercentage()) != 0 + || (loanCharge.isSpecifiedDueDate() && !loanCharge.getDueLocalDate().equals(chargeData.getDueDate()))) { + isChargeModified = true; + } + } + } + + Set possiblyModifedLoanCollateralItems = null; + + if (command.parameterExists("loanType")) { + final String loanTypeStr = command.stringValueOfParameterNamed("loanType"); + final AccountType loanType = AccountType.fromName(loanTypeStr); + + if (!StringUtils.isBlank(loanTypeStr) && loanType.isIndividualAccount()) { + possiblyModifedLoanCollateralItems = this.loanCollateralAssembler.fromParsedJson(command.parsedJson()); + } + } + this.loanScheduleAssembler.updateLoanApplicationAttributes(command, loan, changes); + + if (!changes.isEmpty()) { + final boolean recalculateLoanSchedule = !(changes.size() == 1 + && changes.containsKey(LoanApiConstants.inArrearsToleranceParameterName)); + changes.put(Loan.RECALCULATE_LOAN_SCHEDULE, recalculateLoanSchedule); + isChargeModified = true; + } + + final String dateFormatAsInput = command.dateFormat(); + final String localeAsInput = command.locale(); + + if (command.isChangeInStringParameterNamed(LoanApiConstants.accountNoParameterName, loan.getAccountNumber())) { + final String newValue = command.stringValueOfParameterNamed(LoanApiConstants.accountNoParameterName); + changes.put(LoanApiConstants.accountNoParameterName, newValue); + loan.setAccountNumber(StringUtils.defaultIfEmpty(newValue, null)); + } + + if (command.isChangeInBooleanParameterNamed(LoanApiConstants.createStandingInstructionAtDisbursementParameterName, + loan.shouldCreateStandingInstructionAtDisbursement())) { + final Boolean valueAsInput = command + .booleanObjectValueOfParameterNamed(LoanApiConstants.createStandingInstructionAtDisbursementParameterName); + changes.put(LoanApiConstants.createStandingInstructionAtDisbursementParameterName, valueAsInput); + loan.setCreateStandingInstructionAtDisbursement(valueAsInput); + } + + if (command.isChangeInStringParameterNamed(LoanApiConstants.externalIdParameterName, loan.getExternalId().getValue())) { + final String newValue = command.stringValueOfParameterNamed(LoanApiConstants.externalIdParameterName); + ExternalId externalId = ExternalIdFactory.produce(newValue); + if (externalId.isEmpty() && TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled()) { + externalId = ExternalId.generate(); + } + changes.put(LoanApiConstants.externalIdParameterName, externalId); + loan.setExternalId(externalId); + } + + // add clientId, groupId and loanType changes to actual changes + + final Long clientId = loan.getClient() == null ? null : loan.getClient().getId(); + if (command.isChangeInLongParameterNamed(LoanApiConstants.clientIdParameterName, clientId)) { + final Long newValue = command.longValueOfParameterNamed(LoanApiConstants.clientIdParameterName); + changes.put(LoanApiConstants.clientIdParameterName, newValue); + final Client client = this.clientRepository.findOneWithNotFoundDetection(newValue); + loan.updateClient(client); + } + + // FIXME: AA - We may require separate api command to move loan from one + // group to another + final Long groupId = loan.getGroup() == null ? null : loan.getGroup().getId(); + if (command.isChangeInLongParameterNamed(LoanApiConstants.groupIdParameterName, groupId)) { + final Long newValue = command.longValueOfParameterNamed(LoanApiConstants.groupIdParameterName); + changes.put(LoanApiConstants.groupIdParameterName, newValue); + final Group group = this.groupRepository.findOneWithNotFoundDetection(newValue); + loan.updateGroup(group); + } + + if (command.isChangeInLongParameterNamed(LoanApiConstants.productIdParameterName, loan.getLoanProduct().getId())) { + final Long newValue = command.longValueOfParameterNamed(LoanApiConstants.productIdParameterName); + changes.put(LoanApiConstants.productIdParameterName, newValue); + loan.updateLoanProduct(loanProduct); + final MonetaryCurrency currency = new MonetaryCurrency(loanProduct.getCurrency().getCode(), + loanProduct.getCurrency().getDigitsAfterDecimal(), loanProduct.getCurrency().getCurrencyInMultiplesOf()); + loan.getLoanRepaymentScheduleDetail().setCurrency(currency); + + if (!changes.containsKey(LoanApiConstants.interestRateFrequencyTypeParameterName)) { + loan.updateInterestRateFrequencyType(); + } + + if (loanProduct.isLinkedToFloatingInterestRate()) { + loan.getLoanProductRelatedDetail().updateForFloatingInterestRates(); + } else { + loan.setInterestRateDifferential(null); + loan.setIsFloatingInterestRate(null); + } + loan.updateIsInterestRecalculationEnabled(); + changes.put(Loan.RECALCULATE_LOAN_SCHEDULE, true); + } + + if (command.isChangeInBooleanParameterNamed(LoanApiConstants.isFloatingInterestRateParameterName, + loan.getIsFloatingInterestRate())) { + final Boolean newValue = command.booleanObjectValueOfParameterNamed(LoanApiConstants.isFloatingInterestRateParameterName); + changes.put(LoanApiConstants.isFloatingInterestRateParameterName, newValue); + loan.setIsFloatingInterestRate(newValue); + } + + if (command.isChangeInBigDecimalParameterNamed(LoanApiConstants.interestRateDifferentialParameterName, + loan.getInterestRateDifferential())) { + final BigDecimal newValue = command.bigDecimalValueOfParameterNamed(LoanApiConstants.interestRateDifferentialParameterName); + changes.put(LoanApiConstants.interestRateDifferentialParameterName, newValue); + loan.setInterestRateDifferential(newValue); + } + + Long existingFundId = null; + if (loan.getFund() != null) { + existingFundId = loan.getFund().getId(); + } + if (command.isChangeInLongParameterNamed(LoanApiConstants.fundIdParameterName, existingFundId)) { + final Long newValue = command.longValueOfParameterNamed(LoanApiConstants.fundIdParameterName); + changes.put(LoanApiConstants.fundIdParameterName, newValue); + final Fund fund = findFundByIdIfProvided(newValue); + loan.updateFund(fund); + } + + Long existingLoanOfficerId = null; + if (loan.getLoanOfficer() != null) { + existingLoanOfficerId = loan.getLoanOfficer().getId(); + } + + if (command.isChangeInLongParameterNamed(LoanApiConstants.loanOfficerIdParameterName, existingLoanOfficerId)) { + final Long newValue = command.longValueOfParameterNamed(LoanApiConstants.loanOfficerIdParameterName); + changes.put(LoanApiConstants.loanOfficerIdParameterName, newValue); + final Staff newOfficer = findLoanOfficerByIdIfProvided(newValue); + loan.updateLoanOfficerOnLoanApplication(newOfficer); + } + + Long existingLoanPurposeId = null; + if (loan.getLoanPurpose() != null) { + existingLoanPurposeId = loan.getLoanPurpose().getId(); + } + + if (command.isChangeInLongParameterNamed(LoanApiConstants.loanPurposeIdParameterName, existingLoanPurposeId)) { + final Long newValue = command.longValueOfParameterNamed(LoanApiConstants.loanPurposeIdParameterName); + changes.put(LoanApiConstants.loanPurposeIdParameterName, newValue); + final CodeValue loanPurpose = findCodeValueByIdIfProvided(newValue); + loan.updateLoanPurpose(loanPurpose); + } + + if (command.isChangeInStringParameterNamed(LoanApiConstants.transactionProcessingStrategyCodeParameterName, + loan.getTransactionProcessingStrategyCode()) + && loanProduct.getLoanConfigurableAttributes().getTransactionProcessingStrategyBoolean()) { + final String newValue = command.stringValueOfParameterNamed(LoanApiConstants.transactionProcessingStrategyCodeParameterName); + + final String transactionProcessingStrategyCode = command.stringValueOfParameterNamed("transactionProcessingStrategyCode"); + final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory + .determineProcessor(transactionProcessingStrategyCode); + changes.put(LoanApiConstants.transactionProcessingStrategyCodeParameterName, newValue); + loan.updateTransactionProcessingStrategy(transactionProcessingStrategyCode, + loanRepaymentScheduleTransactionProcessor.getName()); + } + + if (command.isChangeInLocalDateParameterNamed(LoanApiConstants.submittedOnDateParameterName, loan.getSubmittedOnDate())) { + final String valueAsInput = command.stringValueOfParameterNamed(LoanApiConstants.submittedOnDateParameterName); + changes.put(LoanApiConstants.submittedOnDateParameterName, valueAsInput); + changes.put(LoanApiConstants.dateFormatParameterName, dateFormatAsInput); + changes.put(LoanApiConstants.localeParameterName, localeAsInput); + loan.setSubmittedOnDate(command.localDateValueOfParameterNamed(LoanApiConstants.submittedOnDateParameterName)); + } + + if (command.isChangeInLocalDateParameterNamed(LoanApiConstants.expectedDisbursementDateParameterName, + loan.getExpectedDisbursedOnLocalDate())) { + final String valueAsInput = command.stringValueOfParameterNamed(LoanApiConstants.expectedDisbursementDateParameterName); + changes.put(LoanApiConstants.expectedDisbursementDateParameterName, valueAsInput); + changes.put(LoanApiConstants.dateFormatParameterName, dateFormatAsInput); + changes.put(LoanApiConstants.localeParameterName, localeAsInput); + changes.put(Loan.RECALCULATE_LOAN_SCHEDULE, true); + loan.setExpectedDisbursementDate( + command.localDateValueOfParameterNamed(LoanApiConstants.expectedDisbursementDateParameterName)); + } + + if (command.isChangeInLocalDateParameterNamed(LoanApiConstants.repaymentsStartingFromDateParameterName, + loan.getExpectedFirstRepaymentOnDate())) { + final String valueAsInput = command.stringValueOfParameterNamed(LoanApiConstants.repaymentsStartingFromDateParameterName); + changes.put(LoanApiConstants.repaymentsStartingFromDateParameterName, valueAsInput); + changes.put(LoanApiConstants.dateFormatParameterName, dateFormatAsInput); + changes.put(LoanApiConstants.localeParameterName, localeAsInput); + changes.put(Loan.RECALCULATE_LOAN_SCHEDULE, true); + loan.setExpectedFirstRepaymentOnDate( + command.localDateValueOfParameterNamed(LoanApiConstants.repaymentsStartingFromDateParameterName)); + } + + if (command.isChangeInBooleanParameterNamed(LoanApiConstants.syncDisbursementWithMeetingParameterName, + loan.isSyncDisbursementWithMeeting())) { + final Boolean valueAsInput = command + .booleanObjectValueOfParameterNamed(LoanApiConstants.syncDisbursementWithMeetingParameterName); + changes.put(LoanApiConstants.syncDisbursementWithMeetingParameterName, valueAsInput); + loan.setSyncDisbursementWithMeeting(valueAsInput); + } + + if (command.isChangeInLocalDateParameterNamed(LoanApiConstants.interestChargedFromDateParameterName, + loan.getInterestChargedFromDate())) { + final String valueAsInput = command.stringValueOfParameterNamed(LoanApiConstants.interestChargedFromDateParameterName); + changes.put(LoanApiConstants.interestChargedFromDateParameterName, valueAsInput); + changes.put(LoanApiConstants.dateFormatParameterName, dateFormatAsInput); + changes.put(LoanApiConstants.localeParameterName, localeAsInput); + changes.put(Loan.RECALCULATE_LOAN_SCHEDULE, true); + loan.setInterestChargedFromDate(command.localDateValueOfParameterNamed(LoanApiConstants.interestChargedFromDateParameterName)); + } + + if (isChargeModified) { + changes.put(LoanApiConstants.chargesParameterName, loanChargeMapper.map(possiblyModifiedLoanCharges, loan.getCurrency())); + changes.put(Loan.RECALCULATE_LOAN_SCHEDULE, true); + } + + if (command.parameterExists(LoanApiConstants.collateralParameterName) && possiblyModifedLoanCollateralItems != null + && possiblyModifedLoanCollateralItems.equals(loan.getLoanCollateralManagements())) { + changes.put(LoanApiConstants.collateralParameterName, loanCollateralManagementMapper.map(possiblyModifedLoanCollateralItems)); + } + + if (command.isChangeInIntegerParameterNamed(LoanApiConstants.loanTermFrequencyParameterName, loan.getTermFrequency())) { + final Integer newValue = command.integerValueOfParameterNamed(LoanApiConstants.loanTermFrequencyParameterName); + changes.put(LoanApiConstants.loanTermFrequencyParameterName, newValue); + loan.setTermFrequency(newValue); + } + + if (command.isChangeInIntegerParameterNamed(LoanApiConstants.loanTermFrequencyTypeParameterName, + loan.getTermPeriodFrequencyType())) { + final Integer newValue = command.integerValueOfParameterNamed(LoanApiConstants.loanTermFrequencyTypeParameterName); + changes.put(LoanApiConstants.loanTermFrequencyTypeParameterName, newValue); + loan.setTermPeriodFrequencyType(newValue); + } + + if (command.isChangeInBigDecimalParameterNamed(LoanApiConstants.principalParameterName, loan.getApprovedPrincipal())) { + loan.setApprovedPrincipal(command.bigDecimalValueOfParameterNamed(LoanApiConstants.principalParameterName)); + } + + if (command.isChangeInBigDecimalParameterNamed(LoanApiConstants.principalParameterName, loan.getProposedPrincipal())) { + BigDecimal newValue = command.bigDecimalValueOfParameterNamed(LoanApiConstants.principalParameterName); + changes.put(LoanApiConstants.principalParameterName, newValue); + loan.setProposedPrincipal(newValue); + } + + if (loanProduct.isMultiDisburseLoan()) { + loan.updateDisbursementDetails(command, changes); + if (command.isChangeInBigDecimalParameterNamed(LoanApiConstants.maxOutstandingBalanceParameterName, + loan.getMaxOutstandingLoanBalance())) { + loan.setMaxOutstandingLoanBalance( + command.bigDecimalValueOfParameterNamed(LoanApiConstants.maxOutstandingBalanceParameterName)); + } + final JsonArray disbursementDataArray = command.arrayOfParameterNamed(LoanApiConstants.disbursementDataParameterName); + + if (loanProduct.isDisallowExpectedDisbursements()) { + if (disbursementDataArray != null && !disbursementDataArray.isEmpty()) { + final String errorMessage = "For this loan product, disbursement details are not allowed"; + throw new MultiDisbursementDataNotAllowedException(LoanApiConstants.disbursementDataParameterName, errorMessage); + } + } else { + if (disbursementDataArray == null || disbursementDataArray.isEmpty()) { + final String errorMessage = "For this loan product, disbursement details must be provided"; + throw new MultiDisbursementDataRequiredException(LoanApiConstants.disbursementDataParameterName, errorMessage); + } + + if (disbursementDataArray.size() > loanProduct.maxTrancheCount()) { + final String errorMessage = "Number of tranche shouldn't be greter than " + loanProduct.maxTrancheCount(); + throw new ExceedingTrancheCountException(LoanApiConstants.disbursementDataParameterName, errorMessage, + loanProduct.maxTrancheCount(), disbursementDetails.size()); + } + } + } else { + loan.clearDisbursementDetails(); + } + + if (loanProduct.isMultiDisburseLoan() || loanProduct.isCanDefineInstallmentAmount()) { + if (command.isChangeInBigDecimalParameterNamed(LoanApiConstants.fixedEmiAmountParameterName, loan.getFixedEmiAmount())) { + loan.setFixedEmiAmount(command.bigDecimalValueOfParameterNamed(LoanApiConstants.fixedEmiAmountParameterName)); + changes.put(LoanApiConstants.fixedEmiAmountParameterName, loan.getFixedEmiAmount()); + changes.put(Loan.RECALCULATE_LOAN_SCHEDULE, true); + } + } else { + loan.setFixedEmiAmount(null); + } + + if (command.isChangeInBigDecimalParameterNamed(LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName, + loan.getFixedPrincipalPercentagePerInstallment())) { + loan.setFixedPrincipalPercentagePerInstallment( + command.bigDecimalValueOfParameterNamed(LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName)); + changes.put(LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName, loan.getFixedPrincipalPercentagePerInstallment()); + } + + final LoanProductRelatedDetail productRelatedDetail = loan.repaymentScheduleDetail(); + if (loan.loanProduct().getLoanConfigurableAttributes() != null) { + loanScheduleAssembler.updateProductRelatedDetails(productRelatedDetail, loan); + } + + if (loan.getLoanProduct().isCanUseForTopup() && loan.getClientId() != null) { + final Boolean isTopup = command.booleanObjectValueOfParameterNamed(LoanApiConstants.isTopup); + if (command.isChangeInBooleanParameterNamed(LoanApiConstants.isTopup, loan.isTopup())) { + loan.setIsTopup(isTopup); + changes.put(LoanApiConstants.isTopup, isTopup); + } + + if (loan.isTopup()) { + final Long loanIdToClose = command.longValueOfParameterNamed(LoanApiConstants.loanIdToClose); + LoanTopupDetails existingLoanTopupDetails = loan.getTopupLoanDetails(); + if (existingLoanTopupDetails == null || !existingLoanTopupDetails.getLoanIdToClose().equals(loanIdToClose) + || changes.containsKey("submittedOnDate") || changes.containsKey("expectedDisbursementDate") + || changes.containsKey("principal") || changes.containsKey(LoanApiConstants.disbursementDataParameterName)) { + Long existingLoanIdToClose = null; + if (existingLoanTopupDetails != null) { + existingLoanIdToClose = existingLoanTopupDetails.getLoanIdToClose(); + } + + if (!loanIdToClose.equals(existingLoanIdToClose)) { + final LoanTopupDetails topupDetails = new LoanTopupDetails(loan, loanIdToClose); + loan.setTopupLoanDetails(topupDetails); + changes.put(LoanApiConstants.loanIdToClose, loanIdToClose); + } + } + } else { + loan.setTopupLoanDetails(null); + } + } else { + if (loan.isTopup()) { + loan.setIsTopup(false); + loan.setTopupLoanDetails(null); + changes.put(LoanApiConstants.isTopup, false); + } + } + + /** + * TODO: Allow other loan types if needed. + */ + if (command.parameterExists("loanType")) { + final String loanTypeStr = command.stringValueOfParameterNamed("loanType"); + final AccountType loanType = AccountType.fromName(loanTypeStr); + + if (!StringUtils.isBlank(loanTypeStr) && loanType.isIndividualAccount()) { + final String collateralParamName = "collateral"; + if (changes.containsKey(collateralParamName)) { + loan.updateLoanCollateral(possiblyModifedLoanCollateralItems); + } + } + } + + final String chargesParamName = "charges"; + if (changes.containsKey(chargesParamName)) { + loan.updateLoanCharges(possiblyModifiedLoanCharges); + } + + // update installment level delinquency + if (command.isChangeInBooleanParameterNamed(LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY, + loan.isEnableInstallmentLevelDelinquency())) { + final Boolean enableInstallmentLevelDelinquency = command + .booleanObjectValueOfParameterNamed(LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY); + loan.updateEnableInstallmentLevelDelinquency(enableInstallmentLevelDelinquency); + } + + if (changes.containsKey("recalculateLoanSchedule")) { + changes.remove("recalculateLoanSchedule"); + + final JsonElement parsedQuery = this.fromApiJsonHelper.parse(command.json()); + final JsonQuery query = JsonQuery.from(command.json(), parsedQuery, this.fromApiJsonHelper); + + final LoanScheduleModel loanSchedule = this.calculationPlatformService.calculateLoanSchedule(query, false); + loan.updateLoanSchedule(loanSchedule); + loan.recalculateAllCharges(); + } + + // Changes to modify loan rates. + if (command.hasParameter(LoanProductConstants.RATES_PARAM_NAME)) { + loan.updateLoanRates(rateAssembler.fromParsedJson(command.parsedJson())); + } + + return changes; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeAssembler.java index ebfbd5cd2..cbeea9a55 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeAssembler.java @@ -40,7 +40,7 @@ import org.apache.fineract.portfolio.charge.domain.ChargeCalculationType; import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode; import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper; import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; -import org.apache.fineract.portfolio.charge.exception.LoanChargeCannotBeAddedException; +import org.apache.fineract.portfolio.charge.exception.LoanChargeNotFoundException; import org.apache.fineract.portfolio.charge.exception.LoanChargeWithoutMandatoryFieldException; import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; import org.apache.fineract.portfolio.loanaccount.domain.Loan; @@ -65,13 +65,13 @@ public class LoanChargeAssembler { JsonArray jsonDisbursement = this.fromApiJsonHelper.extractJsonArrayNamed("disbursementData", element); List disbursementChargeIds = new ArrayList<>(); - if (jsonDisbursement != null && jsonDisbursement.size() > 0) { + if (jsonDisbursement != null && !jsonDisbursement.isEmpty()) { for (int i = 0; i < jsonDisbursement.size(); i++) { final JsonObject jsonObject = jsonDisbursement.get(i).getAsJsonObject(); if (jsonObject != null && jsonObject.getAsJsonPrimitive(LoanApiConstants.loanChargeIdParameterName) != null) { String chargeIds = jsonObject.getAsJsonPrimitive(LoanApiConstants.loanChargeIdParameterName).getAsString(); if (chargeIds != null) { - if (chargeIds.indexOf(",") != -1) { + if (chargeIds.contains(",")) { Iterable chargeId = Splitter.on(',').split(chargeIds); for (String loanChargeId : chargeId) { disbursementChargeIds.add(Long.parseLong(loanChargeId)); @@ -104,7 +104,7 @@ public class LoanChargeAssembler { final JsonObject loanChargeElement = array.get(i).getAsJsonObject(); - final Long id = this.fromApiJsonHelper.extractLongNamed("id", loanChargeElement); + final Long loanChargeId = this.fromApiJsonHelper.extractLongNamed("id", loanChargeElement); final Long chargeId = this.fromApiJsonHelper.extractLongNamed("chargeId", loanChargeElement); final BigDecimal amount = this.fromApiJsonHelper.extractBigDecimalNamed("amount", loanChargeElement, locale); final Integer chargeTimeType = this.fromApiJsonHelper.extractIntegerNamed("chargeTimeType", loanChargeElement, locale); @@ -116,16 +116,8 @@ public class LoanChargeAssembler { locale); final String externalIdStr = this.fromApiJsonHelper.extractStringNamed("externalId", loanChargeElement); final ExternalId externalId = externalIdFactory.create(externalIdStr); - if (id == null) { + if (loanChargeId == null) { final Charge chargeDefinition = this.chargeRepository.findOneWithNotFoundDetection(chargeId); - - if (chargeDefinition.isOverdueInstallment()) { - - final String defaultUserMessage = "Installment charge cannot be added to the loan."; - throw new LoanChargeCannotBeAddedException("loanCharge", "overdue.charge", defaultUserMessage, null, - chargeDefinition.getName()); - } - ChargeTimeType chargeTime = null; if (chargeTimeType != null) { chargeTime = ChargeTimeType.fromInt(chargeTimeType); @@ -145,7 +137,7 @@ public class LoanChargeAssembler { } else { if (topLevelJsonElement.has("disbursementData") && topLevelJsonElement.get("disbursementData").isJsonArray()) { final JsonArray disbursementArray = topLevelJsonElement.get("disbursementData").getAsJsonArray(); - if (disbursementArray.size() > 0) { + if (!disbursementArray.isEmpty()) { JsonObject disbursementDataElement = disbursementArray.get(0).getAsJsonObject(); expectedDisbursementDate = this.fromApiJsonHelper.extractLocalDateNamed( LoanApiConstants.expectedDisbursementDateParameterName, disbursementDataElement, dateFormat, @@ -201,14 +193,14 @@ public class LoanChargeAssembler { } } } else { - final Long loanChargeId = id; - final LoanCharge loanCharge = this.loanChargeRepository.findById(loanChargeId).orElse(null); - if (loanCharge != null) { - if (!loanCharge.isTrancheDisbursementCharge() || disbursementChargeIds.contains(loanChargeId)) { - loanCharge.update(amount, dueDate, numberOfRepayments); - loanCharges.add(loanCharge); - } + final LoanCharge loanCharge = this.loanChargeRepository.findById(loanChargeId) + .orElseThrow(() -> new LoanChargeNotFoundException(loanChargeId)); + + if (!loanCharge.isTrancheDisbursementCharge() || disbursementChargeIds.contains(loanChargeId)) { + loanCharge.update(amount, dueDate, numberOfRepayments); + loanCharges.add(loanCharge); } + } } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java index 8fce7d71e..ef7152c94 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java @@ -70,7 +70,6 @@ import org.apache.fineract.portfolio.account.domain.AccountTransferType; import org.apache.fineract.portfolio.account.exception.AccountTransferNotFoundException; import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformService; import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService; -import org.apache.fineract.portfolio.accountdetails.domain.AccountType; import org.apache.fineract.portfolio.charge.domain.Charge; import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper; import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; @@ -710,7 +709,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo paymentDetail); // Update loan transaction on repayment. - if (AccountType.fromInt(loan.getLoanType()).isIndividualAccount()) { + if (loan.getLoanType().isIndividualAccount()) { Set loanCollateralManagements = loan.getLoanCollateralManagements(); for (LoanCollateralManagement loanCollateralManagement : loanCollateralManagements) { loanCollateralManagement.setLoanTransactionData(loanTransaction); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDisbursementDetailsAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDisbursementDetailsAssembler.java new file mode 100644 index 000000000..45a510b40 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDisbursementDetailsAssembler.java @@ -0,0 +1,59 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.service; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; +import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails; + +@RequiredArgsConstructor +public class LoanDisbursementDetailsAssembler { + + private final FromJsonHelper fromApiJsonHelper; + + public List fetchDisbursementData(final JsonObject command) { + final Locale locale = this.fromApiJsonHelper.extractLocaleParameter(command); + final String dateFormat = this.fromApiJsonHelper.extractDateFormatParameter(command); + List disbursementDatas = new ArrayList<>(); + JsonArray disbursementDataArray = command.getAsJsonArray(LoanApiConstants.disbursementDataParameterName); + if (disbursementDataArray != null && !disbursementDataArray.isEmpty()) { + disbursementDataArray.forEach(jsonElement -> { + LocalDate expectedDisbursementDate = this.fromApiJsonHelper + .extractLocalDateNamed(LoanApiConstants.expectedDisbursementDateParameterName, jsonElement, dateFormat, locale); + BigDecimal principal = this.fromApiJsonHelper.extractBigDecimalNamed(LoanApiConstants.disbursementPrincipalParameterName, + jsonElement, locale); + BigDecimal netDisbursalAmount = this.fromApiJsonHelper + .extractBigDecimalNamed(LoanApiConstants.disbursementNetDisbursalAmountParameterName, jsonElement, locale); + boolean isReversed = Boolean.TRUE.equals( + this.fromApiJsonHelper.extractBooleanNamed(LoanApiConstants.disbursementReversedParameterName, jsonElement)); + disbursementDatas + .add(new LoanDisbursementDetails(expectedDisbursementDate, null, principal, netDisbursalAmount, isReversed)); + }); + } + return disbursementDatas; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanUtilService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanUtilService.java index a247bc5ad..614f8f24a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanUtilService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanUtilService.java @@ -18,19 +18,13 @@ */ package org.apache.fineract.portfolio.loanaccount.service; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; import java.math.BigDecimal; import java.time.LocalDate; -import java.util.ArrayList; import java.util.List; -import java.util.Locale; import java.util.Set; import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.exception.PlatformServiceUnavailableException; -import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.organisation.holiday.domain.Holiday; import org.apache.fineract.organisation.holiday.domain.HolidayRepository; import org.apache.fineract.organisation.holiday.domain.HolidayStatusType; @@ -52,11 +46,9 @@ import org.apache.fineract.portfolio.floatingrates.data.FloatingRatePeriodData; import org.apache.fineract.portfolio.floatingrates.exception.FloatingRateNotFoundException; import org.apache.fineract.portfolio.floatingrates.service.FloatingRatesReadPlatformService; import org.apache.fineract.portfolio.group.domain.Group; -import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; import org.apache.fineract.portfolio.loanaccount.domain.Loan; -import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; @@ -71,7 +63,6 @@ public class LoanUtilService { private final WorkingDaysRepositoryWrapper workingDaysRepository; private final LoanScheduleGeneratorFactory loanScheduleFactory; private final FloatingRatesReadPlatformService floatingRatesReadPlatformService; - private final FromJsonHelper fromApiJsonHelper; private final CalendarReadPlatformService calendarReadPlatformService; public ScheduleGeneratorDTO buildScheduleGeneratorDTO(final Loan loan, final LocalDate recalculateFrom) { @@ -288,51 +279,6 @@ public class LoanUtilService { return calculatedRepaymentsStartingFromDate; } - public List fetchDisbursementData(final JsonObject command) { - final Locale locale = this.fromApiJsonHelper.extractLocaleParameter(command); - final String dateFormat = this.fromApiJsonHelper.extractDateFormatParameter(command); - List disbursementDatas = new ArrayList<>(); - if (command.has(LoanApiConstants.disbursementDataParameterName)) { - final JsonArray disbursementDataArray = command.getAsJsonArray(LoanApiConstants.disbursementDataParameterName); - if (disbursementDataArray != null && disbursementDataArray.size() > 0) { - int i = 0; - do { - final JsonObject jsonObject = disbursementDataArray.get(i).getAsJsonObject(); - LocalDate expectedDisbursementDate = null; - LocalDate actualDisbursementDate = null; - BigDecimal principal = null; - BigDecimal netDisbursalAmount = null; - - if (jsonObject.has(LoanApiConstants.expectedDisbursementDateParameterName)) { - expectedDisbursementDate = this.fromApiJsonHelper.extractLocalDateNamed( - LoanApiConstants.expectedDisbursementDateParameterName, jsonObject, dateFormat, locale); - } - if (jsonObject.has(LoanApiConstants.disbursementPrincipalParameterName) - && jsonObject.get(LoanApiConstants.disbursementPrincipalParameterName).isJsonPrimitive() - && StringUtils.isNotBlank(jsonObject.get(LoanApiConstants.disbursementPrincipalParameterName).getAsString())) { - principal = jsonObject.getAsJsonPrimitive(LoanApiConstants.disbursementPrincipalParameterName).getAsBigDecimal(); - } - if (jsonObject.has(LoanApiConstants.disbursementNetDisbursalAmountParameterName) - && jsonObject.get(LoanApiConstants.disbursementNetDisbursalAmountParameterName).isJsonPrimitive() - && StringUtils.isNotBlank( - jsonObject.get(LoanApiConstants.disbursementNetDisbursalAmountParameterName).getAsString())) { - netDisbursalAmount = jsonObject.getAsJsonPrimitive(LoanApiConstants.disbursementNetDisbursalAmountParameterName) - .getAsBigDecimal(); - } - boolean isReversed = false; - if (jsonObject.has(LoanApiConstants.disbursementReversedParameterName)) { - isReversed = this.fromApiJsonHelper.extractBooleanNamed(LoanApiConstants.disbursementReversedParameterName, - jsonObject); - } - disbursementDatas.add(new LoanDisbursementDetails(expectedDisbursementDate, actualDisbursementDate, principal, - netDisbursalAmount, isReversed)); - i++; - } while (i < disbursementDataArray.size()); - } - } - return disbursementDatas; - } - public void validateRepaymentTransactionType(LoanTransactionType repaymentTransactionType) { if (!repaymentTransactionType.isRepaymentType()) { throw new PlatformServiceUnavailableException("error.msg.repaymentTransactionType.provided.not.a.repayment.type", diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index 696980d58..2743623a4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -147,7 +147,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagement; import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails; -import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetailsRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent; import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; @@ -180,7 +179,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanSchedul import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod; import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleHistoryWritePlatformService; import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequest; -import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationCommandFromApiJsonHelper; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationValidator; import org.apache.fineract.portfolio.loanaccount.serialization.LoanEventApiJsonValidator; import org.apache.fineract.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer; import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; @@ -225,7 +224,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf private final FromJsonHelper fromApiJsonHelper; private final CalendarRepository calendarRepository; private final LoanScheduleHistoryWritePlatformService loanScheduleHistoryWritePlatformService; - private final LoanApplicationCommandFromApiJsonHelper loanApplicationCommandFromApiJsonHelper; + private final LoanApplicationValidator loanApplicationCommandFromApiJsonHelper; private final AccountAssociationsRepository accountAssociationRepository; private final AccountTransferDetailRepository accountTransferDetailRepository; private final BusinessEventNotifierService businessEventNotifierService; @@ -240,7 +239,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf private final LoanRepository loanRepository; private final RepaymentWithPostDatedChecksAssembler repaymentWithPostDatedChecksAssembler; private final PostDatedChecksRepository postDatedChecksRepository; - private final LoanDisbursementDetailsRepository loanDisbursementDetailsRepository; private final LoanRepaymentScheduleInstallmentRepository loanRepaymentScheduleInstallmentRepository; private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine; private final LoanAccountLockService loanAccountLockService; @@ -327,8 +325,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final Set loanCollateralManagements = loan.getLoanCollateralManagements(); // Get relevant loan collateral modules - if ((loanCollateralManagements != null && !loanCollateralManagements.isEmpty()) - && AccountType.fromInt(loan.getLoanType()).isIndividualAccount()) { + if ((loanCollateralManagements != null && !loanCollateralManagements.isEmpty()) && loan.getLoanType().isIndividualAccount()) { BigDecimal totalCollateral = BigDecimal.valueOf(0); @@ -996,7 +993,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf holidayDetailDto, isHolidayValidationDone); loan = loanTransaction.getLoan(); // Update loan transaction on repayment. - if (AccountType.fromInt(loan.getLoanType()).isIndividualAccount()) { + if (loan.getLoanType().isIndividualAccount()) { Set loanCollateralManagements = loan.getLoanCollateralManagements(); for (LoanCollateralManagement loanCollateralManagement : loanCollateralManagements) { loanCollateralManagement.setLoanTransactionData(loanTransaction); @@ -1597,7 +1594,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf businessEventNotifierService.notifyPostBusinessEvent(new LoanCloseBusinessEvent(loan)); // Update loan transaction on repayment. - if (AccountType.fromInt(loan.getLoanType()).isIndividualAccount()) { + if (loan.getLoanType().isIndividualAccount()) { Set loanCollateralManagements = loan.getLoanCollateralManagements(); for (LoanCollateralManagement loanCollateralManagement : loanCollateralManagements) { ClientCollateralManagement clientCollateralManagement = loanCollateralManagement.getClientCollateralManagement(); @@ -1678,7 +1675,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf this.loanAccountDomainService.disableStandingInstructionsLinkedToClosedLoan(loan); // Update loan transaction on repayment. - if (AccountType.fromInt(loan.getLoanType()).isIndividualAccount()) { + if (loan.getLoanType().isIndividualAccount()) { Set loanCollateralManagements = loan.getLoanCollateralManagements(); for (LoanCollateralManagement loanCollateralManagement : loanCollateralManagements) { ClientCollateralManagement clientCollateralManagement = loanCollateralManagement.getClientCollateralManagement(); @@ -2486,7 +2483,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final ScheduleGeneratorDTO scheduleGeneratorDTO, final LocalDate nextPossibleRepaymentDate, final LocalDate rescheduledRepaymentDate) { final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed("actualDisbursementDate"); - BigDecimal emiAmount = command.bigDecimalValueOfParameterNamed(LoanApiConstants.emiAmountParameterName); + BigDecimal emiAmount = command.bigDecimalValueOfParameterNamed(LoanApiConstants.fixedEmiAmountParameterName); loan.regenerateScheduleOnDisbursement(scheduleGeneratorDTO, recalculateSchedule, actualDisbursementDate, emiAmount, nextPossibleRepaymentDate, rescheduledRepaymentDate); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java index 501e98582..33a32d0e2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java @@ -24,15 +24,12 @@ import org.apache.fineract.infrastructure.accountnumberformat.domain.AccountNumb import org.apache.fineract.infrastructure.codes.domain.CodeValueRepositoryWrapper; import org.apache.fineract.infrastructure.codes.service.CodeValueReadPlatformService; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; -import org.apache.fineract.infrastructure.configuration.domain.GlobalConfigurationRepositoryWrapper; import org.apache.fineract.infrastructure.core.exception.ErrorHandler; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.infrastructure.core.service.PaginationHelper; import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; import org.apache.fineract.infrastructure.dataqueries.service.EntityDatatableChecksWritePlatformService; -import org.apache.fineract.infrastructure.entityaccess.domain.FineractEntityRelationRepository; -import org.apache.fineract.infrastructure.entityaccess.domain.FineractEntityToEntityMappingRepository; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.infrastructure.security.utils.ColumnValidator; @@ -65,14 +62,12 @@ import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatform import org.apache.fineract.portfolio.floatingrates.service.FloatingRatesReadPlatformService; import org.apache.fineract.portfolio.fund.domain.FundRepository; import org.apache.fineract.portfolio.fund.service.FundReadPlatformService; -import org.apache.fineract.portfolio.group.domain.GroupRepository; import org.apache.fineract.portfolio.group.domain.GroupRepositoryWrapper; import org.apache.fineract.portfolio.group.service.GroupReadPlatformService; import org.apache.fineract.portfolio.loanaccount.domain.GLIMAccountInfoRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService; import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainServiceJpa; import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeRepository; -import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetailsRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallmentRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; @@ -82,14 +77,15 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanSummaryWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanaccount.guarantor.service.GuarantorDomainService; -import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.AprCalculator; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory; import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleAssembler; import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleCalculationPlatformService; import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleHistoryWritePlatformService; +import org.apache.fineract.portfolio.loanaccount.mapper.LoanChargeMapper; +import org.apache.fineract.portfolio.loanaccount.mapper.LoanCollateralManagementMapper; import org.apache.fineract.portfolio.loanaccount.mapper.LoanTransactionRelationMapper; -import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationCommandFromApiJsonHelper; import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationTransitionApiJsonValidator; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationValidator; import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeApiJsonValidator; import org.apache.fineract.portfolio.loanaccount.serialization.LoanEventApiJsonValidator; import org.apache.fineract.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer; @@ -118,6 +114,7 @@ import org.apache.fineract.portfolio.loanaccount.service.LoanChargeReadPlatformS import org.apache.fineract.portfolio.loanaccount.service.LoanChargeReadPlatformServiceImpl; import org.apache.fineract.portfolio.loanaccount.service.LoanChargeWritePlatformService; import org.apache.fineract.portfolio.loanaccount.service.LoanChargeWritePlatformServiceImpl; +import org.apache.fineract.portfolio.loanaccount.service.LoanDisbursementDetailsAssembler; import org.apache.fineract.portfolio.loanaccount.service.LoanDownPaymentHandlerService; import org.apache.fineract.portfolio.loanaccount.service.LoanDownPaymentHandlerServiceImpl; import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; @@ -131,7 +128,6 @@ import org.apache.fineract.portfolio.loanaccount.service.RecalculateInterestPost import org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusinessEventService; import org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusinessEventServiceImpl; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository; -import org.apache.fineract.portfolio.loanproduct.serialization.LoanProductDataValidator; import org.apache.fineract.portfolio.loanproduct.service.LoanDropdownReadPlatformService; import org.apache.fineract.portfolio.loanproduct.service.LoanProductReadPlatformService; import org.apache.fineract.portfolio.note.domain.NoteRepository; @@ -140,7 +136,7 @@ import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeReadPlatform import org.apache.fineract.portfolio.rate.service.RateAssembler; import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecksRepository; import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.service.RepaymentWithPostDatedChecksAssembler; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper; import org.apache.fineract.portfolio.savings.service.GSIMReadPlatformService; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; @@ -168,11 +164,8 @@ public class LoanAccountConfiguration { @Bean @ConditionalOnMissingBean(GLIMAccountInfoWritePlatformService.class) - public GLIMAccountInfoWritePlatformService glimAccountInfoWritePlatformService(PlatformSecurityContext context, - GLIMAccountInfoRepository glimAccountRepository, - - LoanRepository loanRepository) { - return new GLIMAccountInfoWritePlatformServiceImpl(context, glimAccountRepository, loanRepository); + public GLIMAccountInfoWritePlatformService glimAccountInfoWritePlatformService(GLIMAccountInfoRepository glimAccountRepository) { + return new GLIMAccountInfoWritePlatformServiceImpl(glimAccountRepository); } @Bean @@ -210,38 +203,23 @@ public class LoanAccountConfiguration { @ConditionalOnMissingBean(LoanApplicationWritePlatformService.class) public LoanApplicationWritePlatformService loanApplicationWritePlatformService(PlatformSecurityContext context, FromJsonHelper fromJsonHelper, LoanApplicationTransitionApiJsonValidator loanApplicationTransitionApiJsonValidator, - LoanProductDataValidator loanProductCommandFromApiJsonDeserializer, - LoanApplicationCommandFromApiJsonHelper fromApiJsonDeserializer, LoanRepositoryWrapper loanRepositoryWrapper, - NoteRepository noteRepository, LoanScheduleCalculationPlatformService calculationPlatformService, LoanAssembler loanAssembler, - ClientRepositoryWrapper clientRepository, LoanProductRepository loanProductRepository, LoanChargeAssembler loanChargeAssembler, - LoanCollateralAssembler loanCollateralAssembler, AprCalculator aprCalculator, AccountNumberGenerator accountNumberGenerator, - LoanSummaryWrapper loanSummaryWrapper, GroupRepositoryWrapper groupRepository, + LoanApplicationValidator loanApplicationValidator, LoanRepositoryWrapper loanRepositoryWrapper, NoteRepository noteRepository, + LoanAssembler loanAssembler, LoanSummaryWrapper loanSummaryWrapper, LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory, CalendarRepository calendarRepository, CalendarInstanceRepository calendarInstanceRepository, - SavingsAccountAssembler savingsAccountAssembler, AccountAssociationsRepository accountAssociationsRepository, - LoanReadPlatformService loanReadPlatformService, AccountNumberFormatRepositoryWrapper accountNumberFormatRepository, - BusinessEventNotifierService businessEventNotifierService, ConfigurationDomainService configurationDomainService, - LoanScheduleAssembler loanScheduleAssembler, LoanUtilService loanUtilService, - CalendarReadPlatformService calendarReadPlatformService, - EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService, - GlobalConfigurationRepositoryWrapper globalConfigurationRepository, - FineractEntityToEntityMappingRepository entityMappingRepository, - FineractEntityRelationRepository fineractEntityRelationRepository, - LoanProductReadPlatformService loanProductReadPlatformService, - - RateAssembler rateAssembler, GLIMAccountInfoWritePlatformService glimAccountInfoWritePlatformService, - GLIMAccountInfoRepository glimRepository, LoanRepository loanRepository, GSIMReadPlatformService gsimReadPlatformService, - LoanLifecycleStateMachine defaultLoanLifecycleStateMachine, LoanProductDataValidator loanProductDataValidator) { + SavingsAccountRepositoryWrapper savingsAccountRepository, AccountAssociationsRepository accountAssociationsRepository, + LoanReadPlatformService loanReadPlatformService, BusinessEventNotifierService businessEventNotifierService, + ConfigurationDomainService configurationDomainService, LoanScheduleAssembler loanScheduleAssembler, + LoanUtilService loanUtilService, CalendarReadPlatformService calendarReadPlatformService, + EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService, GLIMAccountInfoRepository glimRepository, + LoanRepository loanRepository, GSIMReadPlatformService gsimReadPlatformService, + LoanLifecycleStateMachine defaultLoanLifecycleStateMachine) { return new LoanApplicationWritePlatformServiceJpaRepositoryImpl(context, fromJsonHelper, loanApplicationTransitionApiJsonValidator, - loanProductCommandFromApiJsonDeserializer, fromApiJsonDeserializer, loanRepositoryWrapper, noteRepository, - calculationPlatformService, loanAssembler, clientRepository, loanProductRepository, loanChargeAssembler, - loanCollateralAssembler, aprCalculator, accountNumberGenerator, loanSummaryWrapper, groupRepository, - loanRepaymentScheduleTransactionProcessorFactory, calendarRepository, calendarInstanceRepository, savingsAccountAssembler, - accountAssociationsRepository, loanReadPlatformService, accountNumberFormatRepository, businessEventNotifierService, - configurationDomainService, loanScheduleAssembler, loanUtilService, calendarReadPlatformService, - entityDatatableChecksWritePlatformService, globalConfigurationRepository, entityMappingRepository, - fineractEntityRelationRepository, loanProductReadPlatformService, rateAssembler, glimAccountInfoWritePlatformService, - glimRepository, loanRepository, gsimReadPlatformService, defaultLoanLifecycleStateMachine, loanProductDataValidator); + loanApplicationValidator, loanRepositoryWrapper, noteRepository, loanAssembler, loanSummaryWrapper, + loanRepaymentScheduleTransactionProcessorFactory, calendarRepository, calendarInstanceRepository, savingsAccountRepository, + accountAssociationsRepository, loanReadPlatformService, businessEventNotifierService, configurationDomainService, + loanScheduleAssembler, loanUtilService, calendarReadPlatformService, entityDatatableChecksWritePlatformService, + glimRepository, loanRepository, gsimReadPlatformService, defaultLoanLifecycleStateMachine); } @Bean @@ -254,18 +232,25 @@ public class LoanAccountConfiguration { @Bean @ConditionalOnMissingBean(LoanAssembler.class) public LoanAssembler loanAssembler(FromJsonHelper fromApiJsonHelper, LoanRepositoryWrapper loanRepository, - LoanProductRepository loanProductRepository, ClientRepositoryWrapper clientRepository, GroupRepository groupRepository, + LoanProductRepository loanProductRepository, ClientRepositoryWrapper clientRepository, GroupRepositoryWrapper groupRepository, FundRepository fundRepository, StaffRepository staffRepository, CodeValueRepositoryWrapper codeValueRepository, LoanScheduleAssembler loanScheduleAssembler, LoanChargeAssembler loanChargeAssembler, LoanCollateralAssembler collateralAssembler, LoanSummaryWrapper loanSummaryWrapper, LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory, HolidayRepository holidayRepository, ConfigurationDomainService configurationDomainService, - WorkingDaysRepositoryWrapper workingDaysRepository, LoanUtilService loanUtilService, RateAssembler rateAssembler, - LoanLifecycleStateMachine defaultLoanLifecycleStateMachine, ExternalIdFactory externalIdFactory) { + WorkingDaysRepositoryWrapper workingDaysRepository, RateAssembler rateAssembler, + LoanLifecycleStateMachine defaultLoanLifecycleStateMachine, ExternalIdFactory externalIdFactory, + AccountNumberFormatRepositoryWrapper accountNumberFormatRepository, GLIMAccountInfoRepository glimRepository, + AccountNumberGenerator accountNumberGenerator, GLIMAccountInfoWritePlatformService glimAccountInfoWritePlatformService, + LoanCollateralAssembler loanCollateralAssembler, LoanScheduleCalculationPlatformService calculationPlatformService, + LoanDisbursementDetailsAssembler loanDisbursementDetailsAssembler, LoanChargeMapper loanChargeMapper, + LoanCollateralManagementMapper loanCollateralManagementMapper) { return new LoanAssembler(fromApiJsonHelper, loanRepository, loanProductRepository, clientRepository, groupRepository, fundRepository, staffRepository, codeValueRepository, loanScheduleAssembler, loanChargeAssembler, collateralAssembler, loanSummaryWrapper, loanRepaymentScheduleTransactionProcessorFactory, holidayRepository, configurationDomainService, - workingDaysRepository, loanUtilService, rateAssembler, defaultLoanLifecycleStateMachine, externalIdFactory); + workingDaysRepository, rateAssembler, defaultLoanLifecycleStateMachine, externalIdFactory, accountNumberFormatRepository, + glimRepository, accountNumberGenerator, glimAccountInfoWritePlatformService, loanCollateralAssembler, + calculationPlatformService, loanDisbursementDetailsAssembler, loanChargeMapper, loanCollateralManagementMapper); } @Bean @@ -369,10 +354,9 @@ public class LoanAccountConfiguration { CalendarInstanceRepository calendarInstanceRepository, ConfigurationDomainService configurationDomainService, HolidayRepository holidayRepository, WorkingDaysRepositoryWrapper workingDaysRepository, LoanScheduleGeneratorFactory loanScheduleFactory, FloatingRatesReadPlatformService floatingRatesReadPlatformService, - FromJsonHelper fromApiJsonHelper, CalendarReadPlatformService calendarReadPlatformService) { + CalendarReadPlatformService calendarReadPlatformService) { return new LoanUtilService(applicationCurrencyRepository, calendarInstanceRepository, configurationDomainService, holidayRepository, - workingDaysRepository, loanScheduleFactory, floatingRatesReadPlatformService, fromApiJsonHelper, - calendarReadPlatformService); + workingDaysRepository, loanScheduleFactory, floatingRatesReadPlatformService, calendarReadPlatformService); } @Bean @@ -390,15 +374,14 @@ public class LoanAccountConfiguration { AccountAssociationsReadPlatformService accountAssociationsReadPlatformService, LoanReadPlatformService loanReadPlatformService, FromJsonHelper fromApiJsonHelper, CalendarRepository calendarRepository, LoanScheduleHistoryWritePlatformService loanScheduleHistoryWritePlatformService, - LoanApplicationCommandFromApiJsonHelper loanApplicationCommandFromApiJsonHelper, - AccountAssociationsRepository accountAssociationRepository, AccountTransferDetailRepository accountTransferDetailRepository, - BusinessEventNotifierService businessEventNotifierService, GuarantorDomainService guarantorDomainService, - LoanUtilService loanUtilService, LoanSummaryWrapper loanSummaryWrapper, + LoanApplicationValidator loanApplicationValidator, AccountAssociationsRepository accountAssociationRepository, + AccountTransferDetailRepository accountTransferDetailRepository, BusinessEventNotifierService businessEventNotifierService, + GuarantorDomainService guarantorDomainService, LoanUtilService loanUtilService, LoanSummaryWrapper loanSummaryWrapper, EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService, LoanRepaymentScheduleTransactionProcessorFactory transactionProcessingStrategy, CodeValueRepositoryWrapper codeValueRepository, CashierTransactionDataValidator cashierTransactionDataValidator, GLIMAccountInfoRepository glimRepository, LoanRepository loanRepository, RepaymentWithPostDatedChecksAssembler repaymentWithPostDatedChecksAssembler, - PostDatedChecksRepository postDatedChecksRepository, LoanDisbursementDetailsRepository loanDisbursementDetailsRepository, + PostDatedChecksRepository postDatedChecksRepository, LoanRepaymentScheduleInstallmentRepository loanRepaymentScheduleInstallmentRepository, LoanLifecycleStateMachine defaultLoanLifecycleStateMachine, LoanAccountLockService loanAccountLockService, ExternalIdFactory externalIdFactory, ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService, @@ -410,11 +393,10 @@ public class LoanAccountConfiguration { paymentDetailWritePlatformService, holidayRepository, configurationDomainService, workingDaysRepository, accountTransfersWritePlatformService, accountTransfersReadPlatformService, accountAssociationsReadPlatformService, loanReadPlatformService, fromApiJsonHelper, calendarRepository, loanScheduleHistoryWritePlatformService, - loanApplicationCommandFromApiJsonHelper, accountAssociationRepository, accountTransferDetailRepository, - businessEventNotifierService, guarantorDomainService, loanUtilService, loanSummaryWrapper, - entityDatatableChecksWritePlatformService, transactionProcessingStrategy, codeValueRepository, - cashierTransactionDataValidator, glimRepository, loanRepository, repaymentWithPostDatedChecksAssembler, - postDatedChecksRepository, loanDisbursementDetailsRepository, loanRepaymentScheduleInstallmentRepository, + loanApplicationValidator, accountAssociationRepository, accountTransferDetailRepository, businessEventNotifierService, + guarantorDomainService, loanUtilService, loanSummaryWrapper, entityDatatableChecksWritePlatformService, + transactionProcessingStrategy, codeValueRepository, cashierTransactionDataValidator, glimRepository, loanRepository, + repaymentWithPostDatedChecksAssembler, postDatedChecksRepository, loanRepaymentScheduleInstallmentRepository, defaultLoanLifecycleStateMachine, loanAccountLockService, externalIdFactory, replayedTransactionBusinessEventService, loanAccrualTransactionBusinessEventService, errorHandler, loanDownPaymentHandlerService); } @@ -439,4 +421,10 @@ public class LoanAccountConfiguration { BusinessEventNotifierService businessEventNotifierService) { return new LoanDownPaymentHandlerServiceImpl(loanTransactionRepository, businessEventNotifierService); } + + @Bean + @ConditionalOnMissingBean(LoanDisbursementDetailsAssembler.class) + public LoanDisbursementDetailsAssembler loanDisbursementDetailsAssembler(FromJsonHelper fromApiJsonHelper) { + return new LoanDisbursementDetailsAssembler(fromApiJsonHelper); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java index b1d6d3d34..e35499b2b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java @@ -48,7 +48,6 @@ import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; -import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanproduct.LoanProductConstants; @@ -401,7 +400,7 @@ public final class LoanProductDataValidator { } // interest rates - BigDecimal interestRatePerPeriod = null; + boolean isInterestBearing = false; if (this.fromApiJsonHelper.parameterExists(IS_LINKED_TO_FLOATING_INTEREST_RATES, element) && this.fromApiJsonHelper.extractBooleanNamed(IS_LINKED_TO_FLOATING_INTEREST_RATES, element)) { if (isEqualAmortization) { @@ -485,6 +484,7 @@ public final class LoanProductDataValidator { .extractBooleanNamed(IS_FLOATING_INTEREST_RATE_CALCULATION_ALLOWED, element); baseDataValidator.reset().parameter(IS_FLOATING_INTEREST_RATE_CALCULATION_ALLOWED) .value(isFloatingInterestRateCalculationAllowed).notNull().isOneOfTheseValues(true, false); + isInterestBearing = true; } else { if (this.fromApiJsonHelper.parameterExists(FLOATING_RATES_ID, element)) { baseDataValidator.reset().parameter(FLOATING_RATES_ID).failWithCode( @@ -522,7 +522,7 @@ public final class LoanProductDataValidator { "isFloatingInterestRateCalculationAllowed param is not supported when isLinkedToFloatingInterestRates is not supplied or false"); } - interestRatePerPeriod = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(INTEREST_RATE_PER_PERIOD, element); + BigDecimal interestRatePerPeriod = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(INTEREST_RATE_PER_PERIOD, element); baseDataValidator.reset().parameter(INTEREST_RATE_PER_PERIOD).value(interestRatePerPeriod).notNull().zeroOrPositiveAmount(); final String minInterestRatePerPeriodParameterName = MIN_INTEREST_RATE_PER_PERIOD; @@ -564,10 +564,12 @@ public final class LoanProductDataValidator { Locale.getDefault()); baseDataValidator.reset().parameter(INTEREST_RATE_FREQUENCY_TYPE).value(interestRateFrequencyType).notNull().inMinMaxRange(0, 4); + isInterestBearing = interestRatePerPeriod.compareTo(BigDecimal.ZERO) > 0; } // Fixed Length validation - fixedLengthValidations(transactionProcessingStrategyCode, interestRatePerPeriod, numberOfRepayments, repaymentEvery, element, + + fixedLengthValidations(transactionProcessingStrategyCode, isInterestBearing, numberOfRepayments, repaymentEvery, element, baseDataValidator); // Guarantee Funds @@ -819,6 +821,10 @@ public final class LoanProductDataValidator { advancedPaymentAllocationsValidator.checkGroupingOfAllocationRules(advancedPaymentAllocationsJsonParser .assembleLoanProductPaymentAllocationRules(command, transactionProcessingStrategyCode)); } + final Integer recurringMoratoriumOnPrincipalPeriods = this.fromApiJsonHelper + .extractIntegerWithLocaleNamed("recurringMoratoriumOnPrincipalPeriods", element); + validateRepaymentPeriodWithGraceSettings(numberOfRepayments, graceOnPrincipalPayment, graceOnInterestPayment, + graceOnInterestCharged, recurringMoratoriumOnPrincipalPeriods, baseDataValidator); throwExceptionIfValidationWarningsExist(dataValidationErrors); } @@ -1267,19 +1273,21 @@ public final class LoanProductDataValidator { } // grace validation + Integer graceOnPrincipalPayment = loanProduct.getLoanProductRelatedDetail().getGraceOnPrincipalPayment(); if (this.fromApiJsonHelper.parameterExists(GRACE_ON_PRINCIPAL_PAYMENT, element)) { - final Integer graceOnPrincipalPayment = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(GRACE_ON_PRINCIPAL_PAYMENT, - element); + graceOnPrincipalPayment = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(GRACE_ON_PRINCIPAL_PAYMENT, element); baseDataValidator.reset().parameter(GRACE_ON_PRINCIPAL_PAYMENT).value(graceOnPrincipalPayment).zeroOrPositiveAmount(); } + Integer graceOnInterestPayment = loanProduct.getLoanProductRelatedDetail().getGraceOnInterestPayment(); if (this.fromApiJsonHelper.parameterExists(GRACE_ON_INTEREST_PAYMENT, element)) { - final Integer graceOnInterestPayment = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(GRACE_ON_INTEREST_PAYMENT, element); + graceOnInterestPayment = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(GRACE_ON_INTEREST_PAYMENT, element); baseDataValidator.reset().parameter(GRACE_ON_INTEREST_PAYMENT).value(graceOnInterestPayment).zeroOrPositiveAmount(); } + Integer graceOnInterestCharged = loanProduct.getLoanProductRelatedDetail().getGraceOnInterestCharged(); if (this.fromApiJsonHelper.parameterExists(GRACE_ON_INTEREST_CHARGED, element)) { - final Integer graceOnInterestCharged = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(GRACE_ON_INTEREST_CHARGED, element); + graceOnInterestCharged = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(GRACE_ON_INTEREST_CHARGED, element); baseDataValidator.reset().parameter(GRACE_ON_INTEREST_CHARGED).value(graceOnInterestCharged).zeroOrPositiveAmount(); } @@ -1368,11 +1376,11 @@ public final class LoanProductDataValidator { } // interest rates - BigDecimal interestRatePerPeriod = null; boolean isLinkedToFloatingInterestRates = loanProduct.isLinkedToFloatingInterestRate(); if (this.fromApiJsonHelper.parameterExists(IS_LINKED_TO_FLOATING_INTEREST_RATES, element)) { isLinkedToFloatingInterestRates = this.fromApiJsonHelper.extractBooleanNamed(IS_LINKED_TO_FLOATING_INTEREST_RATES, element); } + boolean isInterestBearing = false; if (isLinkedToFloatingInterestRates) { if (isEqualAmortization) { throw new EqualAmortizationUnsupportedFeatureException("floating.interest.rate", "floating interest rate"); @@ -1481,6 +1489,7 @@ public final class LoanProductDataValidator { } baseDataValidator.reset().parameter(IS_FLOATING_INTEREST_RATE_CALCULATION_ALLOWED) .value(isFloatingInterestRateCalculationAllowed).notNull().isOneOfTheseValues(true, false); + isInterestBearing = true; } else { if (this.fromApiJsonHelper.parameterExists(FLOATING_RATES_ID, element)) { baseDataValidator.reset().parameter(FLOATING_RATES_ID).failWithCode( @@ -1536,7 +1545,7 @@ public final class LoanProductDataValidator { baseDataValidator.reset().parameter(maxInterestRatePerPeriodParameterName).value(maxInterestRatePerPeriod).ignoreIfNull() .zeroOrPositiveAmount(); - interestRatePerPeriod = loanProduct.getLoanProductRelatedDetail().getNominalInterestRatePerPeriod(); + BigDecimal interestRatePerPeriod = loanProduct.getLoanProductRelatedDetail().getNominalInterestRatePerPeriod(); if (this.fromApiJsonHelper.parameterExists(INTEREST_RATE_PER_PERIOD, element)) { interestRatePerPeriod = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(INTEREST_RATE_PER_PERIOD, element); } @@ -1549,10 +1558,11 @@ public final class LoanProductDataValidator { } baseDataValidator.reset().parameter(INTEREST_RATE_FREQUENCY_TYPE).value(interestRateFrequencyType).notNull().inMinMaxRange(0, 4); + isInterestBearing = interestRatePerPeriod.compareTo(BigDecimal.ZERO) > 0; } // Fixed Length validation - fixedLengthValidations(transactionProcessingStrategyCode, interestRatePerPeriod, numberOfRepayments, repaymentEvery, element, + fixedLengthValidations(transactionProcessingStrategyCode, isInterestBearing, numberOfRepayments, repaymentEvery, element, baseDataValidator); // Guarantee Funds @@ -1806,6 +1816,11 @@ public final class LoanProductDataValidator { "supported.only.for.progressive.loan.schedule.handling", "Vertical repayment schedule processing is only available with `Advanced payment allocation` strategy"); } + final Integer recurringMoratoriumOnPrincipalPeriods = this.fromApiJsonHelper + .extractIntegerWithLocaleNamed("recurringMoratoriumOnPrincipalPeriods", element); + + validateRepaymentPeriodWithGraceSettings(numberOfRepayments, graceOnPrincipalPayment, graceOnInterestPayment, + graceOnInterestCharged, recurringMoratoriumOnPrincipalPeriods, baseDataValidator); throwExceptionIfValidationWarningsExist(dataValidationErrors); } @@ -2400,13 +2415,7 @@ public final class LoanProductDataValidator { } } - public void fixedLengthValidations(final String transactionProcessingStrategyCode, final LoanApplicationTerms loanApplicationTerms, - final JsonElement element, final DataValidatorBuilder baseDataValidator) { - fixedLengthValidations(transactionProcessingStrategyCode, loanApplicationTerms.getAnnualNominalInterestRate(), - loanApplicationTerms.getNumberOfRepayments(), loanApplicationTerms.getRepaymentEvery(), element, baseDataValidator); - } - - public void fixedLengthValidations(final String transactionProcessingStrategyCode, final BigDecimal interestRatePerPeriod, + public void fixedLengthValidations(final String transactionProcessingStrategyCode, final boolean thereIsInterest, final Integer numberOfRepayments, final Integer repayEvery, final JsonElement element, final DataValidatorBuilder baseDataValidator) { if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.FIXED_LENGTH, element)) { @@ -2420,15 +2429,14 @@ public final class LoanProductDataValidator { if (!AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY .equals(transactionProcessingStrategyCode)) { final String errorMsg = "Fixed Length configuration is only allowed with " - + AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY + " stratefy"; + + AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY + " strategy"; throw new GeneralPlatformDomainRuleException("error.msg.fixed.length.only.supported.for.advanced.payment.allocation", errorMsg, AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY); } - if (interestRatePerPeriod.compareTo(BigDecimal.ZERO) > 0) { + if (thereIsInterest) { final String errorMsg = "Fixed Length configuration is only allowed for zero interest products"; - throw new GeneralPlatformDomainRuleException("error.msg.fixed.length.only.supported.for.zero.interest", errorMsg, - interestRatePerPeriod); + throw new GeneralPlatformDomainRuleException("error.msg.fixed.length.only.supported.for.zero.interest", errorMsg); } final Integer valueToCompare = ((numberOfRepayments - 1) * repayEvery) + 1; @@ -2465,4 +2473,45 @@ public final class LoanProductDataValidator { } } + public void validateRepaymentPeriodWithGraceSettings(final Integer numberOfRepayments, final Integer graceOnPrincipalPayment, + final Integer graceOnInterestPayment, final Integer graceOnInterestCharged, final Integer recurringMoratoriumOnPrincipalPeriods, + DataValidatorBuilder baseDataValidator) { + if (numberOfRepayments <= defaultToZeroIfNull(graceOnPrincipalPayment)) { + baseDataValidator.reset().parameter("graceOnPrincipalPayment").value(graceOnPrincipalPayment) + .failWithCode(".mustBeLessThan.numberOfRepayments"); + } + + if (numberOfRepayments <= defaultToZeroIfNull(graceOnInterestPayment)) { + baseDataValidator.reset().parameter("graceOnInterestPayment").value(graceOnInterestPayment) + .failWithCode(".mustBeLessThan.numberOfRepayments"); + } + + if (numberOfRepayments < defaultToZeroIfNull(graceOnInterestCharged)) { + baseDataValidator.reset().parameter("graceOnInterestCharged").value(graceOnInterestCharged) + .failWithCode(".mustBeLessThan.numberOfRepayments"); + } + + int graceOnPrincipal = 0; + if (graceOnPrincipalPayment != null) { + graceOnPrincipal = graceOnPrincipalPayment; + } + int recurMoratoriumOnPrincipal = 0; + if (recurringMoratoriumOnPrincipalPeriods != null) { + recurMoratoriumOnPrincipal = recurringMoratoriumOnPrincipalPeriods; + } + + if ((recurMoratoriumOnPrincipal > 0) && ((numberOfRepayments - graceOnPrincipal) % (recurMoratoriumOnPrincipal + 1) != 1)) { + baseDataValidator.reset().parameter("graceOnPrincipalPayments.and.recurringMoratoriumOnPrincipalPeriods") + .value(graceOnPrincipal).value(recurMoratoriumOnPrincipal) + .failWithCode("causes.principal.moratorium.for.last.installment"); + } + } + + private Integer defaultToZeroIfNull(final Integer value) { + Integer result = value; + if (value == null) { + result = 0; + } + return result; + } } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java index f08c25ce5..0ea2c32be 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java @@ -72,6 +72,7 @@ import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder; import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.EarlyPaymentLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.FineractStyleLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; @@ -2079,8 +2080,12 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan overpaymentAccount); assertNotNull(localLoanProductId); + Integer localLoanProductId2 = createLoanProduct("500", "15", "4", false, LoanScheduleType.CUMULATIVE, assetAccount, + incomeAccount, expenseAccount, overpaymentAccount); + assertNotNull(localLoanProductId2); + CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class, - () -> applyForLoanApplication(client.getClientId(), localLoanProductId, BigDecimal.valueOf(500.0), 45, 15, 3, + () -> applyForLoanApplication(client.getClientId(), localLoanProductId2, BigDecimal.valueOf(500.0), 45, 15, 3, BigDecimal.ZERO, "01 January 2023", "01 January 2023", FineractStyleLoanRepaymentScheduleTransactionProcessor.STRATEGY_CODE, LoanScheduleProcessingType.VERTICAL.name())); @@ -4396,6 +4401,21 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan return loanTransactionHelper.getLoanProductId(loanProductJSON); } + private static Integer createLoanProduct(final String principal, final String repaymentAfterEvery, final String numberOfRepayments, + boolean autoPayForDownPayment, LoanScheduleType loanScheduleType, final Account... accounts) { + LOG.info("------------------------------CREATING NEW LOAN PRODUCT ---------------------------------------"); + final String loanProductJSON = new LoanProductTestBuilder().withMinPrincipal(principal).withPrincipal(principal) + .withRepaymentTypeAsDays().withRepaymentAfterEvery(repaymentAfterEvery).withNumberOfRepayments(numberOfRepayments) + .withEnableDownPayment(true, "25", autoPayForDownPayment).withinterestRatePerPeriod("0") + .withInterestRateFrequencyTypeAsMonths() + .withRepaymentStrategy(EarlyPaymentLoanRepaymentScheduleTransactionProcessor.STRATEGY_CODE) + .withAmortizationTypeAsEqualPrincipalPayment().withInterestTypeAsFlat().withAccountingRulePeriodicAccrual(accounts) + .withInterestCalculationPeriodTypeAsRepaymentPeriod(true).withInterestTypeAsDecliningBalance().withMultiDisburse() + .withDisallowExpectedDisbursements(true).withLoanScheduleType(loanScheduleType).withDaysInMonth("30").withDaysInYear("365") + .withMoratorium("0", "0").build(null); + return loanTransactionHelper.getLoanProductId(loanProductJSON); + } + private static ArrayList> createLoanProductGetError(final String principal, final String repaymentAfterEvery, final String numberOfRepayments, boolean autoPayForDownPayment, LoanScheduleType loanScheduleType, LoanScheduleProcessingType loanScheduleProcessingType, AdvancedPaymentData allocationRuleData, final Account... accounts) { diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java index 3618bd14d..c7150d369 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java @@ -7479,12 +7479,16 @@ public class ClientLoanIntegrationTest { map.put("amount", amount); } if (charge.get("dueDate") != null) { - map.put("dueDate", charge.get("dueDate")); + map.put("dueDate", DATE_TIME_FORMATTER.format(fromArrayToLocalDate((List) charge.get("dueDate")))); } map.put("chargeId", charge.get("chargeId")); return map; } + private LocalDate fromArrayToLocalDate(List dueDate) { + return LocalDate.of(dueDate.get(0), dueDate.get(1), dueDate.get(2)); + } + private HashMap createTrancheDetail(final String date, final String amount) { HashMap detail = new HashMap(); detail.put("expectedDisbursementDate", date); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanApplicationApprovalTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanApplicationApprovalTest.java index 7a667bb77..6b37a3043 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanApplicationApprovalTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanApplicationApprovalTest.java @@ -155,7 +155,7 @@ public class LoanApplicationApprovalTest { log.info("---------------------------------CLIENT CREATED WITH ID--------------------------------------------------- {}", clientId); final Integer loanProductId = this.loanTransactionHelper - .getLoanProductId(new LoanProductTestBuilder().withInterestTypeAsDecliningBalance().withTranches(true) + .getLoanProductId(new LoanProductTestBuilder().withInterestTypeAsDecliningBalance().withTranches(false) .withInterestCalculationPeriodTypeAsRepaymentPeriod(true).build(null)); log.info("----------------------------------LOAN PRODUCT CREATED WITH ID------------------------------------------- {}", loanProductId);