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 4a8e783d7d7..baf97c99ce7 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 @@ -1836,114 +1836,11 @@ public Map loanApplicationWithdrawnByApplicant(final AppUser cur return actualChanges; } - public Map loanApplicationApproval(final AppUser currentUser, final JsonCommand command, - final JsonArray disbursementDataArray, final LoanLifecycleStateMachine loanLifecycleStateMachine) { - validateAccountStatus(LoanEvent.LOAN_APPROVED); - - final Map actualChanges = new LinkedHashMap<>(); - - /* - * statusEnum is holding the possible new status derived from loanLifecycleStateMachine.transition. - */ - - final LoanStatus newStatusEnum = loanLifecycleStateMachine.dryTransition(LoanEvent.LOAN_APPROVED, this); - - /* - * FIXME: There is no need to check below condition, if loanLifecycleStateMachine.transition is doing it's - * responsibility properly. Better implementation approach is, if code passes invalid combination of states - * (fromState and toState), state machine should return invalidate state and below if condition should check for - * not equal to invalidateState, instead of check new value is same as present value. - */ - - if (!newStatusEnum.hasStateOf(LoanStatus.fromInt(this.loanStatus))) { - loanLifecycleStateMachine.transition(LoanEvent.LOAN_APPROVED, this); - actualChanges.put(PARAM_STATUS, LoanEnumerations.status(this.loanStatus)); - - // only do below if status has changed in the 'approval' case - LocalDate approvedOn = command.localDateValueOfParameterNamed(APPROVED_ON_DATE); - String approvedOnDateChange = command.stringValueOfParameterNamed(APPROVED_ON_DATE); - if (approvedOn == null) { - approvedOn = command.localDateValueOfParameterNamed(EVENT_DATE); - approvedOnDateChange = command.stringValueOfParameterNamed(EVENT_DATE); - } - - LocalDate expectedDisbursementDate = command.localDateValueOfParameterNamed(EXPECTED_DISBURSEMENT_DATE); - - BigDecimal approvedLoanAmount = command.bigDecimalValueOfParameterNamed(LoanApiConstants.approvedLoanAmountParameterName); - if (approvedLoanAmount != null) { - compareApprovedToProposedPrincipal(approvedLoanAmount); - - /* - * All the calculations are done based on the principal amount, so it is necessary to set principal - * amount to approved amount - */ - this.approvedPrincipal = approvedLoanAmount; - - this.loanRepaymentScheduleDetail.setPrincipal(approvedLoanAmount); - actualChanges.put(LoanApiConstants.approvedLoanAmountParameterName, approvedLoanAmount); - actualChanges.put(LoanApiConstants.disbursementPrincipalParameterName, approvedLoanAmount); - actualChanges.put(LoanApiConstants.disbursementNetDisbursalAmountParameterName, netDisbursalAmount); - - if (disbursementDataArray != null) { - updateDisbursementDetails(command, actualChanges); - } - } - - recalculateAllCharges(); - - if (loanProduct.isMultiDisburseLoan()) { - List currentDisbursementDetails = getLoanDisbursementDetails(); - - if (currentDisbursementDetails.size() > loanProduct.maxTrancheCount()) { - final String errorMessage = "Number of tranche shouldn't be greater than " + loanProduct.maxTrancheCount(); - throw new ExceedingTrancheCountException(LoanApiConstants.disbursementDataParameterName, errorMessage, - loanProduct.maxTrancheCount(), currentDisbursementDetails.size()); - } - } - this.approvedOnDate = approvedOn; - this.approvedBy = currentUser; - actualChanges.put(LOCALE, command.locale()); - actualChanges.put(DATE_FORMAT, command.dateFormat()); - actualChanges.put(APPROVED_ON_DATE, approvedOnDateChange); - - final LocalDate submittalDate = this.submittedOnDate; - if (DateUtils.isBefore(approvedOn, submittalDate)) { - final String errorMessage = "The date on which a loan is approved cannot be before its submittal date: " + submittalDate; - throw new InvalidLoanStateTransitionException("approval", "cannot.be.before.submittal.date", errorMessage, - getApprovedOnDate(), submittalDate); - } - - if (expectedDisbursementDate != null) { - this.expectedDisbursementDate = expectedDisbursementDate; - actualChanges.put(EXPECTED_DISBURSEMENT_DATE, this.expectedDisbursementDate); - - if (DateUtils.isBefore(expectedDisbursementDate, approvedOn)) { - final String errorMessage = "The expected disbursement date should be either on or after the approval date: " - + approvedOn; - throw new InvalidLoanStateTransitionException("expecteddisbursal", "should.be.on.or.after.approval.date", errorMessage, - getApprovedOnDate(), expectedDisbursementDate); - } - } - - validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_APPROVED, approvedOn); - - if (DateUtils.isDateInTheFuture(approvedOn)) { - final String errorMessage = "The date on which a loan is approved cannot be in the future."; - throw new InvalidLoanStateTransitionException("approval", "cannot.be.a.future.date", errorMessage, getApprovedOnDate()); - } - - if (this.loanOfficer != null) { - final LoanOfficerAssignmentHistory loanOfficerAssignmentHistory = LoanOfficerAssignmentHistory.createNew(this, - this.loanOfficer, approvedOn); - this.loanOfficerHistory.add(loanOfficerAssignmentHistory); - } - this.adjustNetDisbursalAmount(this.approvedPrincipal); - } - - return actualChanges; + public int getNumberOfDisbursements() { + return getLoanDisbursementDetails().size(); } - private List getLoanDisbursementDetails() { + public List getLoanDisbursementDetails() { List currentDisbursementDetails = getDisbursementDetails(); if (loanProduct.isDisallowExpectedDisbursements()) { if (!currentDisbursementDetails.isEmpty()) { @@ -1959,24 +1856,7 @@ private List getLoanDisbursementDetails() { return currentDisbursementDetails; } - private void compareApprovedToProposedPrincipal(BigDecimal approvedLoanAmount) { - if (this.loanProduct().isDisallowExpectedDisbursements() && this.loanProduct().isAllowApprovedDisbursedAmountsOverApplied()) { - BigDecimal maxApprovedLoanAmount = getOverAppliedMax(); - if (approvedLoanAmount.compareTo(maxApprovedLoanAmount) > 0) { - final String errorMessage = "Loan approved amount can't be greater than maximum applied loan amount calculation."; - throw new InvalidLoanStateTransitionException("approval", - "amount.can't.be.greater.than.maximum.applied.loan.amount.calculation", errorMessage, approvedLoanAmount, - maxApprovedLoanAmount); - } - } else { - if (approvedLoanAmount.compareTo(this.proposedPrincipal) > 0) { - final String errorMessage = "Loan approved amount can't be greater than loan amount demanded."; - throw new InvalidLoanStateTransitionException("approval", "amount.can't.be.greater.than.loan.amount.demanded", errorMessage, - this.proposedPrincipal, approvedLoanAmount); - } - } - } - + @Deprecated // moved to LoanApplicationValidator private BigDecimal getOverAppliedMax() { if ("percentage".equals(getLoanProduct().getOverAppliedCalculationType())) { BigDecimal overAppliedNumber = BigDecimal.valueOf(getLoanProduct().getOverAppliedNumber()); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationTransitionApiJsonValidator.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationTransitionApiJsonValidator.java index 69125cb5ad8..b9610198dc1 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationTransitionApiJsonValidator.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationTransitionApiJsonValidator.java @@ -56,50 +56,6 @@ private void throwExceptionIfValidationWarningsExist(final List disbursementParameters = new HashSet<>( - Arrays.asList(LoanApiConstants.loanIdTobeApproved, LoanApiConstants.approvedLoanAmountParameterName, - LoanApiConstants.approvedOnDateParameterName, LoanApiConstants.disbursementNetDisbursalAmountParameterName, - LoanApiConstants.noteParameterName, LoanApiConstants.localeParameterName, LoanApiConstants.dateFormatParameterName, - LoanApiConstants.disbursementDataParameterName, LoanApiConstants.expectedDisbursementDateParameterName)); - - final Type typeOfMap = new TypeToken>() {}.getType(); - this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, disbursementParameters); - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loanapplication"); - - final JsonElement element = this.fromApiJsonHelper.parse(json); - - final BigDecimal principal = this.fromApiJsonHelper - .extractBigDecimalWithLocaleNamed(LoanApiConstants.approvedLoanAmountParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.approvedLoanAmountParameterName).value(principal).ignoreIfNull() - .positiveAmount(); - - final BigDecimal netDisbursalAmount = this.fromApiJsonHelper - .extractBigDecimalWithLocaleNamed(LoanApiConstants.disbursementNetDisbursalAmountParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.disbursementNetDisbursalAmountParameterName).value(netDisbursalAmount) - .ignoreIfNull().positiveAmount(); - - final LocalDate approvedOnDate = this.fromApiJsonHelper.extractLocalDateNamed(LoanApiConstants.approvedOnDateParameterName, - element); - baseDataValidator.reset().parameter(LoanApiConstants.approvedOnDateParameterName).value(approvedOnDate).notNull(); - - final LocalDate expectedDisbursementDate = this.fromApiJsonHelper - .extractLocalDateNamed(LoanApiConstants.expectedDisbursementDateParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.expectedDisbursementDateParameterName).value(expectedDisbursementDate) - .ignoreIfNull(); - - final String note = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.noteParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.noteParameterName).value(note).notExceedingLengthOf(1000); - - throwExceptionIfValidationWarningsExist(dataValidationErrors); - } public void validateRejection(final String json) { 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 a89d3406a40..b40c262909d 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 @@ -36,6 +36,7 @@ import java.util.TreeSet; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.ApiParameterError; @@ -87,7 +88,12 @@ import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails; +import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent; +import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine; +import org.apache.fineract.portfolio.loanaccount.domain.LoanOfficerAssignmentHistory; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; +import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType; import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; @@ -118,8 +124,16 @@ 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.apache.fineract.useradministration.domain.AppUser; import org.springframework.stereotype.Service; +import static org.apache.fineract.portfolio.loanaccount.domain.Loan.APPROVED_ON_DATE; +import static org.apache.fineract.portfolio.loanaccount.domain.Loan.DATE_FORMAT; +import static org.apache.fineract.portfolio.loanaccount.domain.Loan.EVENT_DATE; +import static org.apache.fineract.portfolio.loanaccount.domain.Loan.EXPECTED_DISBURSEMENT_DATE; +import static org.apache.fineract.portfolio.loanaccount.domain.Loan.LOCALE; +import static org.apache.fineract.portfolio.loanaccount.domain.Loan.PARAM_STATUS; + @Service @RequiredArgsConstructor public class LoanScheduleAssembler { @@ -141,6 +155,8 @@ public class LoanScheduleAssembler { private final CalendarInstanceRepository calendarInstanceRepository; private final LoanUtilService loanUtilService; private final LoanDisbursementDetailsAssembler loanDisbursementDetailsAssembler; + private final LoanRepositoryWrapper loanRepositoryWrapper; + private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine; public LoanApplicationTerms assembleLoanTerms(final JsonElement element) { final Long loanProductId = this.fromApiJsonHelper.extractLongNamed("productId", element); @@ -296,13 +312,6 @@ private LoanApplicationTerms assembleLoanApplicationTermsFrom(final JsonElement if ((loanType.isJLGAccount() || loanType.isGroupAccount()) && calendar != null) { validateRepaymentsStartDateWithMeetingDates(calculatedRepaymentsStartingFromDate, calendar, isSkipMeetingOnFirstDay, numberOfDays); - - /* - * If disbursement is synced on meeting, make sure disbursement date is on a meeting date - */ - if (synchDisbursement != null && synchDisbursement.booleanValue()) { - validateDisbursementDateWithMeetingDates(expectedDisbursementDate, calendar, isSkipMeetingOnFirstDay, numberOfDays); - } } if (RepaymentStartDateType.DISBURSEMENT_DATE.equals(repaymentStartDateType)) { @@ -601,15 +610,6 @@ private void validateRepaymentsStartDateWithMeetingDates(final LocalDate repayme } } - public void validateDisbursementDateWithMeetingDates(final LocalDate expectedDisbursementDate, final Calendar calendar, - Boolean isSkipRepaymentOnFirstMonth, Integer numberOfDays) { - // disbursement date should fall on a meeting date - if (calendar != null && !calendar.isValidRecurringDate(expectedDisbursementDate, isSkipRepaymentOnFirstMonth, numberOfDays)) { - final String errorMessage = "Expected disbursement date '" + expectedDisbursementDate + "' do not fall on a meeting date"; - throw new LoanApplicationDateException("disbursement.date.do.not.match.meeting.date", errorMessage, expectedDisbursementDate); - } - } - private void validateRepaymentFrequencyIsSameAsMeetingFrequency(final Integer meetingFrequency, final Integer repaymentFrequency, final Integer meetingInterval, final Integer repaymentInterval) { // meeting with daily frequency should allow loan products with any frequency. @@ -1382,4 +1382,76 @@ public void updateLoanApplicationAttributes(JsonCommand command, Loan loan, Map< loanProductRelatedDetail.setEqualAmortization(newValue); } } + + public Pair> assembleLoanApproval(AppUser currentUser, JsonCommand command, Long loanId) { + final JsonArray disbursementDataArray = command.arrayOfParameterNamed(LoanApiConstants.disbursementDataParameterName); + final Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true); + + final LoanStatus newStatus = defaultLoanLifecycleStateMachine.dryTransition(LoanEvent.LOAN_APPROVED, loan); + if (newStatus.hasStateOf(loan.getStatus())) { + return Pair.of(loan, Map.of()); // no status change happened + } + + final Map actualChanges = new HashMap<>(); + defaultLoanLifecycleStateMachine.transition(LoanEvent.LOAN_APPROVED, loan); + actualChanges.put(PARAM_STATUS, LoanEnumerations.status(loan.getStatus())); + + LocalDate approvedOn = command.localDateValueOfParameterNamed(APPROVED_ON_DATE); + String approvedOnDateChange = command.stringValueOfParameterNamed(APPROVED_ON_DATE); + if (approvedOn == null) { + approvedOn = command.localDateValueOfParameterNamed(EVENT_DATE); + approvedOnDateChange = command.stringValueOfParameterNamed(EVENT_DATE); + } + + LocalDate expectedDisbursementDate = command.localDateValueOfParameterNamed(EXPECTED_DISBURSEMENT_DATE); + + BigDecimal approvedLoanAmount = command.bigDecimalValueOfParameterNamed(LoanApiConstants.approvedLoanAmountParameterName); + if (approvedLoanAmount != null) { + /* + * All the calculations are done based on the principal amount, so it is necessary to set principal + * amount to approved amount + */ + loan.setApprovedPrincipal(approvedLoanAmount); + loan.getLoanRepaymentScheduleDetail().setPrincipal(approvedLoanAmount); + actualChanges.put(LoanApiConstants.approvedLoanAmountParameterName, approvedLoanAmount); + actualChanges.put(LoanApiConstants.disbursementPrincipalParameterName, approvedLoanAmount); + actualChanges.put(LoanApiConstants.disbursementNetDisbursalAmountParameterName, loan.getNetDisbursalAmount()); + + if (disbursementDataArray != null) { + loan.updateDisbursementDetails(command, actualChanges); + } + } + + loan.recalculateAllCharges(); + + loan.setApprovedOnDate(approvedOn); + loan.setApprovedBy(currentUser); + + actualChanges.put(LOCALE, command.locale()); + actualChanges.put(DATE_FORMAT, command.dateFormat()); + actualChanges.put(APPROVED_ON_DATE, approvedOnDateChange); + + if (expectedDisbursementDate != null) { + loan.setExpectedDisbursementDate(expectedDisbursementDate); + actualChanges.put(EXPECTED_DISBURSEMENT_DATE, expectedDisbursementDate); + } + + if (loan.getLoanOfficer() != null) { + final LoanOfficerAssignmentHistory loanOfficerAssignmentHistory = LoanOfficerAssignmentHistory.createNew(loan, + loan.getLoanOfficer(), approvedOn); + loan.getLoanOfficerHistory().add(loanOfficerAssignmentHistory); + } + + loan.adjustNetDisbursalAmount(loan.getApprovedPrincipal()); + + if (!actualChanges.isEmpty()) { + if (actualChanges.containsKey(LoanApiConstants.approvedLoanAmountParameterName) + || actualChanges.containsKey("recalculateLoanSchedule") + || actualChanges.containsKey("expectedDisbursementDate")) { + loan.regenerateRepaymentSchedule(loanUtilService.buildScheduleGeneratorDTO(loan, null)); + } + } + + return Pair.of(loan, actualChanges); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java index c17ea93a738..1b224d8e889 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java @@ -52,6 +52,9 @@ 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; @@ -68,6 +71,10 @@ 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.domain.Calendar; +import org.apache.fineract.portfolio.calendar.domain.CalendarEntityType; +import org.apache.fineract.portfolio.calendar.domain.CalendarInstance; +import org.apache.fineract.portfolio.calendar.domain.CalendarInstanceRepository; import org.apache.fineract.portfolio.calendar.service.CalendarUtils; import org.apache.fineract.portfolio.client.domain.Client; import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper; @@ -83,9 +90,11 @@ 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.LoanLifecycleStateMachine; 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.LoanTransactionType; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.exception.ExceedingTrancheCountException; @@ -99,6 +108,7 @@ 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.loanaccount.service.LoanUtilService; import org.apache.fineract.portfolio.loanproduct.LoanProductConstants; import org.apache.fineract.portfolio.loanproduct.data.LoanProductData; import org.apache.fineract.portfolio.loanproduct.domain.AdvancedPaymentAllocationsValidator; @@ -182,6 +192,11 @@ public final class LoanApplicationValidator { private final WorkingDaysRepositoryWrapper workingDaysRepository; private final HolidayRepository holidayRepository; private final SavingsAccountRepositoryWrapper savingsAccountRepository; + private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine; + private final LoanSummaryWrapper loanSummaryWrapper; + private final CalendarInstanceRepository calendarInstanceRepository; + private final LoanUtilService loanUtilService; + private final EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService; public void validateForCreate(final Loan loan) { final LocalDate expectedFirstRepaymentOnDate = loan.getExpectedFirstRepaymentOnDate(); @@ -1900,6 +1915,192 @@ public void validateTopupLoan(Loan loan, LocalDate expectedDisbursementDate) { loan.adjustNetDisbursalAmount(netDisbursalAmount); } + public void validateApproval(JsonCommand command, Long loanId) { + String json = command.json(); + if (StringUtils.isBlank(json)) { + throw new InvalidJsonException(); + } + + final Set disbursementParameters = new HashSet<>( + Arrays.asList(LoanApiConstants.loanIdTobeApproved, LoanApiConstants.approvedLoanAmountParameterName, + LoanApiConstants.approvedOnDateParameterName, LoanApiConstants.disbursementNetDisbursalAmountParameterName, + LoanApiConstants.noteParameterName, LoanApiConstants.localeParameterName, LoanApiConstants.dateFormatParameterName, + LoanApiConstants.disbursementDataParameterName, LoanApiConstants.expectedDisbursementDateParameterName)); + + final Type typeOfMap = new TypeToken>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, disbursementParameters); + + validateOrThrow("loanapplication", baseDataValidator -> { + final JsonElement element = this.fromApiJsonHelper.parse(json); + + final BigDecimal principal = this.fromApiJsonHelper + .extractBigDecimalWithLocaleNamed(LoanApiConstants.approvedLoanAmountParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.approvedLoanAmountParameterName).value(principal).ignoreIfNull() + .positiveAmount(); + + final BigDecimal netDisbursalAmount = this.fromApiJsonHelper + .extractBigDecimalWithLocaleNamed(LoanApiConstants.disbursementNetDisbursalAmountParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.disbursementNetDisbursalAmountParameterName).value(netDisbursalAmount) + .ignoreIfNull().positiveAmount(); + + final LocalDate approvedOnDate = this.fromApiJsonHelper.extractLocalDateNamed(LoanApiConstants.approvedOnDateParameterName, + element); + baseDataValidator.reset().parameter(LoanApiConstants.approvedOnDateParameterName).value(approvedOnDate).notNull(); + + LocalDate expectedDisbursementDate = this.fromApiJsonHelper + .extractLocalDateNamed(LoanApiConstants.expectedDisbursementDateParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.expectedDisbursementDateParameterName).value(expectedDisbursementDate) + .ignoreIfNull(); + + final String note = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.noteParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.noteParameterName).value(note).notExceedingLengthOf(1000); + + final Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true); + loan.setHelpers(defaultLoanLifecycleStateMachine, this.loanSummaryWrapper, + this.loanRepaymentScheduleTransactionProcessorFactory); + + final Client client = loan.client(); + if (client != null && client.isNotActive()) { + throw new ClientNotActiveException(client.getId()); + } + final Group group = loan.group(); + if (group != null && group.isNotActive()) { + throw new GroupNotActiveException(group.getId()); + } + + if (expectedDisbursementDate == null) { + expectedDisbursementDate = loan.getExpectedDisbursedOnLocalDate(); + } + + if (DateUtils.isBefore(approvedOnDate, loan.getSubmittedOnDate())) { + final String errorMessage = "The date on which a loan is approved cannot be before its submittal date: " + + loan.getSubmittedOnDate(); + throw new InvalidLoanStateTransitionException("approval", "cannot.be.before.submittal.date", errorMessage, approvedOnDate, + loan.getSubmittedOnDate()); + } + + if (loan.loanProduct().isMultiDisburseLoan()) { + validateLoanMultiDisbursementDate(element, expectedDisbursementDate, principal); + } + + boolean isSkipRepaymentOnFirstMonth; + int numberOfDays = 0; + if (loan.isSyncDisbursementWithMeeting() && (loan.isGroupLoan() || loan.isJLGLoan())) { + Calendar calendar = getCalendarInstance(loan); + isSkipRepaymentOnFirstMonth = isLoanRepaymentsSyncWithMeeting(loan, calendar); + if (isSkipRepaymentOnFirstMonth) { + numberOfDays = configurationDomainService.retreivePeriodInNumberOfDaysForSkipMeetingDate().intValue(); + } + + validateDisbursementDateWithMeetingDates(expectedDisbursementDate, calendar, isSkipRepaymentOnFirstMonth, numberOfDays); + } + + entityDatatableChecksWritePlatformService.runTheCheckForProduct(loanId, EntityTables.LOAN.getName(), + StatusEnum.APPROVE.getCode().longValue(), EntityTables.LOAN.getForeignKeyColumnNameOnDatatable(), loan.productId()); + + if (loan.isTopup() && loan.getClientId() != null) { + validateTopupLoan(loan, expectedDisbursementDate); + } + + if (!loan.getStatus().isSubmittedAndPendingApproval()) { + final String defaultUserMessage = "Loan Account Approval is not allowed. Loan Account is not in submitted and pending approval state."; + final ApiParameterError error = ApiParameterError + .generalError("error.msg.loan.approve.account.is.not.submitted.and.pending.state", defaultUserMessage); + baseDataValidator.getDataValidationErrors().add(error); + } + + BigDecimal approvedLoanAmount = command.bigDecimalValueOfParameterNamed(LoanApiConstants.approvedLoanAmountParameterName); + if (approvedLoanAmount == null) { + compareApprovedToProposedPrincipal(loan, approvedLoanAmount); + } + + LoanProduct loanProduct = loan.getLoanProduct(); + if (loanProduct.isMultiDisburseLoan()) { + int numberOfDisbursements = loan.getNumberOfDisbursements(); + if (numberOfDisbursements > loanProduct.maxTrancheCount()) { + final String errorMessage = "Number of tranche shouldn't be greater than " + loanProduct.maxTrancheCount(); + throw new ExceedingTrancheCountException(LoanApiConstants.disbursementDataParameterName, errorMessage, + loanProduct.maxTrancheCount(), numberOfDisbursements); + } + } + + if (expectedDisbursementDate != null) { + if (DateUtils.isBefore(expectedDisbursementDate, approvedOnDate)) { + final String errorMessage = "The expected disbursement date should be either on or after the approval date: " + + approvedOnDate; + throw new InvalidLoanStateTransitionException("expecteddisbursal", "should.be.on.or.after.approval.date", errorMessage, + approvedOnDate, expectedDisbursementDate); + } + } + + if (client != null && client.getOfficeJoiningDate() != null) { + final LocalDate clientOfficeJoiningDate = client.getOfficeJoiningDate(); + if (DateUtils.isBefore(approvedOnDate, clientOfficeJoiningDate)) { + throw new InvalidLoanStateTransitionException("approval", "cannot.be.before.client.transfer.date", + "The date on which a loan is approved cannot be earlier than client's transfer date to this office", + clientOfficeJoiningDate); + } + } + + if (DateUtils.isDateInTheFuture(approvedOnDate)) { + final String errorMessage = "The date on which a loan is approved cannot be in the future."; + throw new InvalidLoanStateTransitionException("approval", "cannot.be.a.future.date", errorMessage, approvedOnDate); + } + + }); // end validation + } + + private void compareApprovedToProposedPrincipal(Loan loan, BigDecimal approvedLoanAmount) { + if (loan.loanProduct().isDisallowExpectedDisbursements() && loan.loanProduct().isAllowApprovedDisbursedAmountsOverApplied()) { + BigDecimal maxApprovedLoanAmount = getOverAppliedMax(loan); + if (approvedLoanAmount.compareTo(maxApprovedLoanAmount) > 0) { + final String errorMessage = "Loan approved amount can't be greater than maximum applied loan amount calculation."; + throw new InvalidLoanStateTransitionException("approval", + "amount.can't.be.greater.than.maximum.applied.loan.amount.calculation", errorMessage, approvedLoanAmount, + maxApprovedLoanAmount); + } + } else { + if (approvedLoanAmount.compareTo(loan.getProposedPrincipal()) > 0) { + final String errorMessage = "Loan approved amount can't be greater than loan amount demanded."; + throw new InvalidLoanStateTransitionException("approval", "amount.can't.be.greater.than.loan.amount.demanded", errorMessage, + loan.getProposedPrincipal(), approvedLoanAmount); + } + } + } + + private BigDecimal getOverAppliedMax(Loan loan) { + LoanProduct loanProduct = loan.getLoanProduct(); + if ("percentage".equals(loanProduct.getOverAppliedCalculationType())) { + BigDecimal overAppliedNumber = BigDecimal.valueOf(loanProduct.getOverAppliedNumber()); + BigDecimal totalPercentage = BigDecimal.valueOf(1).add(overAppliedNumber.divide(BigDecimal.valueOf(100))); + return loan.getProposedPrincipal().multiply(totalPercentage); + } else { + return loan.getProposedPrincipal().add(BigDecimal.valueOf(loanProduct.getOverAppliedNumber())); + } + } + + /** + * validate disbursement date should fall on a meeting date + */ + public void validateDisbursementDateWithMeetingDates(final LocalDate expectedDisbursementDate, final Calendar calendar, + Boolean isSkipRepaymentOnFirstMonth, Integer numberOfDays) { + if (calendar != null && !calendar.isValidRecurringDate(expectedDisbursementDate, isSkipRepaymentOnFirstMonth, numberOfDays)) { + final String errorMessage = "Expected disbursement date '" + expectedDisbursementDate + "' do not fall on a meeting date"; + throw new LoanApplicationDateException("disbursement.date.do.not.match.meeting.date", errorMessage, expectedDisbursementDate); + } + } + + private Calendar getCalendarInstance(Loan loan) { + CalendarInstance calendarInstance = calendarInstanceRepository.findCalendarInstaneByEntityId(loan.getId(), + CalendarEntityType.LOANS.getValue()); + return calendarInstance != null ? calendarInstance.getCalendar() : null; + } + + private boolean isLoanRepaymentsSyncWithMeeting(Loan loan, Calendar calendar) { + return configurationDomainService.isSkippingMeetingOnFirstDayOfMonthEnabled() + && loanUtilService.isLoanRepaymentsSyncWithMeeting(loan.group(), calendar); + } + public static void validateOrThrow(String resource, Consumer baseDataValidator) { final List dataValidationErrors = new ArrayList<>(); final DataValidatorBuilder dataValidatorBuilder = new DataValidatorBuilder(dataValidationErrors).resource(resource); 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 1ded66f1064..b95d056d829 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 @@ -31,11 +31,13 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.commons.lang3.tuple.Pair; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; @@ -556,62 +558,15 @@ public CommandProcessingResult approveGLIMLoanAppication(final Long loanId, fina @Override public CommandProcessingResult approveApplication(final Long loanId, final JsonCommand command) { final AppUser currentUser = getAppUserIfPresent(); - loanApplicationTransitionApiJsonValidator.validateApproval(command.json()); + loanApplicationValidator.validateApproval(command, loanId); - Loan loan = retrieveLoanBy(loanId); - checkClientOrGroupActive(loan); - - LocalDate expectedDisbursementDate = command.localDateValueOfParameterNamed(LoanApiConstants.expectedDisbursementDateParameterName); - if (expectedDisbursementDate == null) { - expectedDisbursementDate = loan.getExpectedDisbursedOnLocalDate(); - } - - if (loan.loanProduct().isMultiDisburseLoan()) { - final JsonElement element = this.fromJsonHelper.parse(command.json()); - final BigDecimal principal = this.fromJsonHelper.extractBigDecimalWithLocaleNamed("approvedLoanAmount", element); - loanApplicationValidator.validateLoanMultiDisbursementDate(element, expectedDisbursementDate, principal); - } - - final JsonArray disbursementDataArray = command.arrayOfParameterNamed(LoanApiConstants.disbursementDataParameterName); - - boolean isSkipRepaymentOnFirstMonth = false; - int numberOfDays = 0; - if (loan.isSyncDisbursementWithMeeting() && (loan.isGroupLoan() || loan.isJLGLoan())) { - Calendar calendar = getCalendarInstance(loan); - isSkipRepaymentOnFirstMonth = isLoanRepaymentsSyncWithMeeting(loan, calendar); - if (isSkipRepaymentOnFirstMonth) { - numberOfDays = configurationDomainService.retreivePeriodInNumberOfDaysForSkipMeetingDate().intValue(); - } - - loanScheduleAssembler.validateDisbursementDateWithMeetingDates(expectedDisbursementDate, calendar, isSkipRepaymentOnFirstMonth, - numberOfDays); - } - - final Map changes = loan.loanApplicationApproval(currentUser, command, disbursementDataArray, - defaultLoanLifecycleStateMachine); - - entityDatatableChecksWritePlatformService.runTheCheckForProduct(loanId, EntityTables.LOAN.getName(), - StatusEnum.APPROVE.getCode().longValue(), EntityTables.LOAN.getForeignKeyColumnNameOnDatatable(), loan.productId()); + Pair> loanAndChanges = loanScheduleAssembler.assembleLoanApproval(currentUser, command, loanId); + final Loan loan = loanAndChanges.getLeft(); + final Map changes = loanAndChanges.getRight(); if (!changes.isEmpty()) { - if (changes.containsKey(LoanApiConstants.approvedLoanAmountParameterName) || changes.containsKey("recalculateLoanSchedule") - || changes.containsKey("expectedDisbursementDate")) { - loan.regenerateRepaymentSchedule(loanUtilService.buildScheduleGeneratorDTO(loan, null)); - } - - if (loan.isTopup() && loan.getClientId() != null) { - loanApplicationValidator.validateTopupLoan(loan, expectedDisbursementDate); - } - - loan = this.loanRepository.saveAndFlush(loan); - final String noteText = command.stringValueOfParameterNamed("note"); - if (StringUtils.isNotBlank(noteText)) { - final Note note = Note.loanNote(loan, noteText); - changes.put("note", noteText); - noteRepository.save(note); - } - + createNote(noteText, loan).ifPresent(note -> changes.put("note", note)); businessEventNotifierService.notifyPostBusinessEvent(new LoanApprovedBusinessEvent(loan)); } @@ -627,17 +582,6 @@ public CommandProcessingResult approveApplication(final Long loanId, final JsonC .build(); } - private Calendar getCalendarInstance(Loan loan) { - CalendarInstance calendarInstance = calendarInstanceRepository.findCalendarInstaneByEntityId(loan.getId(), - CalendarEntityType.LOANS.getValue()); - return calendarInstance != null ? calendarInstance.getCalendar() : null; - } - - private boolean isLoanRepaymentsSyncWithMeeting(Loan loan, Calendar calendar) { - return configurationDomainService.isSkippingMeetingOnFirstDayOfMonthEnabled() - && loanUtilService.isLoanRepaymentsSyncWithMeeting(loan.group(), calendar); - } - @Transactional @Override public CommandProcessingResult undoGLIMLoanApplicationApproval(final Long loanId, final JsonCommand command) { @@ -916,10 +860,13 @@ private void createCalendar(JsonCommand command, Loan loan) { } } - private void createNote(String submittedOnNote, Loan newLoanApplication) { + private Optional createNote(String submittedOnNote, Loan newLoanApplication) { if (StringUtils.isNotBlank(submittedOnNote)) { final Note note = Note.loanNote(newLoanApplication, submittedOnNote); this.noteRepository.save(note); + return Optional.of(note); + } else { + return Optional.empty(); } }