From 99fbcb376c5744512c4dd77fd0cb88018054a5c6 Mon Sep 17 00:00:00 2001 From: Kristof Jozsa Date: Wed, 19 Jun 2024 14:46:37 +0200 Subject: [PATCH] FINERACT-2090: restructure loan approvals --- .../domain/ConfigurationDomainService.java | 2 +- .../portfolio/loanaccount/domain/Loan.java | 129 +---------- .../LoanApplicationTransitionValidator.java | 29 --- .../domain/ConfigurationDomainServiceJpa.java | 2 +- .../CalendarReadPlatformServiceImpl.java | 4 +- ...ollectionSheetReadPlatformServiceImpl.java | 2 +- .../CenterReadPlatformServiceImpl.java | 2 +- .../service/LoanScheduleAssembler.java | 108 +++++++-- .../LoanApplicationValidator.java | 215 +++++++++++++++++- ...WritePlatformServiceJpaRepositoryImpl.java | 125 +--------- .../loanaccount/service/LoanUtilService.java | 6 +- ...WritePlatformServiceJpaRepositoryImpl.java | 2 +- ...WritePlatformServiceJpaRepositoryImpl.java | 6 +- ...LoanAccountsContainsCurrencyFieldTest.java | 13 +- .../SchedulerJobsTestResults.java | 25 +- ...nitiateExternalAssetOwnerTransferTest.java | 2 +- 16 files changed, 339 insertions(+), 333 deletions(-) diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java index 579b5e2325d..164986e8715 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java @@ -81,7 +81,7 @@ public interface ConfigurationDomainService { boolean isPrincipalCompoundingDisabledForOverdueLoans(); - Long retreivePeroidInNumberOfDaysForSkipMeetingDate(); + Long retreivePeriodInNumberOfDaysForSkipMeetingDate(); boolean isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled(); 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 3f7f49aa149..b317357e81f 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 @@ -114,7 +114,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor.TransactionCtx; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder; -import org.apache.fineract.portfolio.loanaccount.exception.ExceedingTrancheCountException; import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanStateTransitionException; import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException; import org.apache.fineract.portfolio.loanaccount.exception.InvalidRefundDateException; @@ -1789,114 +1788,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()) { @@ -1912,24 +1808,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/LoanApplicationTransitionValidator.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationTransitionValidator.java index a8c34353896..5a701bfc4ff 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationTransitionValidator.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationTransitionValidator.java @@ -113,39 +113,10 @@ public void validateApproval(final String json) { } public void validateRejection(final JsonCommand command, final Loan loan, final LoanLifecycleStateMachine loanLifecycleStateMachine) { - // validate request body - final String json = command.json(); - validateLoanRejectionRequestBody(json); - // validate loan rejection validateLoanRejection(command, loan, loanLifecycleStateMachine); } - private void validateLoanRejectionRequestBody(String json) { - if (StringUtils.isBlank(json)) { - throw new InvalidJsonException(); - } - - final Set disbursementParameters = new HashSet<>(Arrays.asList(LoanApiConstants.rejectedOnDateParameterName, - LoanApiConstants.noteParameterName, LoanApiConstants.localeParameterName, LoanApiConstants.dateFormatParameterName)); - - 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 LocalDate rejectedOnDate = this.fromApiJsonHelper.extractLocalDateNamed(LoanApiConstants.rejectedOnDateParameterName, - element); - baseDataValidator.reset().parameter(LoanApiConstants.rejectedOnDateParameterName).value(rejectedOnDate).notNull(); - - final String note = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.noteParameterName, element); - baseDataValidator.reset().parameter(LoanApiConstants.noteParameterName).value(note).notExceedingLengthOf(1000); - - throwExceptionIfValidationWarningsExist(dataValidationErrors); - } - private void validateLoanRejection(final JsonCommand command, final Loan loan, final LoanLifecycleStateMachine loanLifecycleStateMachine) { // validate client or group is active diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java index 58b0bf3dca9..7c2bf1740fa 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java @@ -284,7 +284,7 @@ public boolean isPrincipalCompoundingDisabledForOverdueLoans() { } @Override - public Long retreivePeroidInNumberOfDaysForSkipMeetingDate() { + public Long retreivePeriodInNumberOfDaysForSkipMeetingDate() { final String propertyName = "skip-repayment-on-first-day-of-month"; final GlobalConfigurationPropertyData property = getGlobalConfigurationPropertyData(propertyName); return property.getValue(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/calendar/service/CalendarReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/calendar/service/CalendarReadPlatformServiceImpl.java index 22bee0c5542..5c4cb8a2680 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/calendar/service/CalendarReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/calendar/service/CalendarReadPlatformServiceImpl.java @@ -249,7 +249,7 @@ private Collection generateRecurringDate(final CalendarData calendarD Integer numberOfDays = 0; boolean isSkipRepaymentOnFirstMonthEnabled = this.configurationDomainService.isSkippingMeetingOnFirstDayOfMonthEnabled(); if (isSkipRepaymentOnFirstMonthEnabled) { - numberOfDays = this.configurationDomainService.retreivePeroidInNumberOfDaysForSkipMeetingDate().intValue(); + numberOfDays = this.configurationDomainService.retreivePeriodInNumberOfDaysForSkipMeetingDate().intValue(); } final Collection recurringDates = CalendarUtils.getRecurringDates(rrule, seedDate, periodStartDate, periodEndDate, @@ -328,7 +328,7 @@ public LocalDate generateNextEligibleMeetingDateForCollection(final CalendarData Integer numberOfDays = 0; boolean isSkipRepaymentOnFirstMonthEnabled = configurationDomainService.isSkippingMeetingOnFirstDayOfMonthEnabled(); if (isSkipRepaymentOnFirstMonthEnabled) { - numberOfDays = configurationDomainService.retreivePeroidInNumberOfDaysForSkipMeetingDate().intValue(); + numberOfDays = configurationDomainService.retreivePeriodInNumberOfDaysForSkipMeetingDate().intValue(); } if (lastMeetingDate != null && !calendarData.isBetweenStartAndEndDate(lastMeetingDate) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionsheet/service/CollectionSheetReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionsheet/service/CollectionSheetReadPlatformServiceImpl.java index 97d7a14892b..1c721ac4d38 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionsheet/service/CollectionSheetReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/collectionsheet/service/CollectionSheetReadPlatformServiceImpl.java @@ -344,7 +344,7 @@ public JLGCollectionSheetData generateGroupCollectionSheet(final Long groupId, f Integer numberOfDays = 0; boolean isSkipRepaymentOnFirstMonthEnabled = this.configurationDomainService.isSkippingMeetingOnFirstDayOfMonthEnabled(); if (isSkipRepaymentOnFirstMonthEnabled) { - numberOfDays = this.configurationDomainService.retreivePeroidInNumberOfDaysForSkipMeetingDate().intValue(); + numberOfDays = this.configurationDomainService.retreivePeriodInNumberOfDaysForSkipMeetingDate().intValue(); isSkipMeetingOnFirstDay = this.calendarReadPlatformService.isCalendarAssociatedWithEntity(entityId, calendar.getId(), entityType.getValue().longValue()); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/service/CenterReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/service/CenterReadPlatformServiceImpl.java index d6fe072d5d5..9175ef44497 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/service/CenterReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/service/CenterReadPlatformServiceImpl.java @@ -510,7 +510,7 @@ public Collection retriveAllCentersByMeetingDate(final Long off Integer numberOfDays = 0; boolean isSkipRepaymentOnFirstMonthEnabled = this.configurationDomainService.isSkippingMeetingOnFirstDayOfMonthEnabled(); if (isSkipRepaymentOnFirstMonthEnabled) { - numberOfDays = this.configurationDomainService.retreivePeroidInNumberOfDaysForSkipMeetingDate().intValue(); + numberOfDays = this.configurationDomainService.retreivePeriodInNumberOfDaysForSkipMeetingDate().intValue(); } for (CenterData centerData : centerDataArray) { if (centerData.getCollectionMeetingCalendar().isValidRecurringDate(meetingDate, isSkipRepaymentOnFirstMonthEnabled, 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 f4a7003fd41..716015764f4 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 @@ -18,6 +18,13 @@ */ package org.apache.fineract.portfolio.loanaccount.loanschedule.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; + import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; @@ -36,6 +43,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 +95,11 @@ 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.LoanTermVariationType; import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; @@ -118,6 +130,7 @@ 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; @Service @@ -141,6 +154,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); @@ -228,7 +243,6 @@ private LoanApplicationTerms assembleLoanApplicationTermsFrom(final JsonElement LocalDate calculatedRepaymentsStartingFromDate = repaymentsStartingFromDate; - final Boolean synchDisbursement = this.fromApiJsonHelper.extractBooleanNamed("syncDisbursementWithMeeting", element); final Long calendarId = this.fromApiJsonHelper.extractLongNamed("calendarId", element); Calendar calendar = null; @@ -290,19 +304,12 @@ private LoanApplicationTerms assembleLoanApplicationTermsFrom(final JsonElement if (isSkipRepaymentOnFirstMonthEnabled) { isSkipMeetingOnFirstDay = this.loanUtilService.isLoanRepaymentsSyncWithMeeting(group, calendar); if (isSkipMeetingOnFirstDay) { - numberOfDays = configurationDomainService.retreivePeroidInNumberOfDaysForSkipMeetingDate().intValue(); + numberOfDays = configurationDomainService.retreivePeriodInNumberOfDaysForSkipMeetingDate().intValue(); } } 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,20 +608,9 @@ 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. + // meeting with daily frequency should allow loan products with any frequency. if (!PeriodFrequencyType.DAYS.getValue().equals(meetingFrequency)) { // repayment frequency must match with meeting frequency if (!meetingFrequency.equals(repaymentFrequency)) { @@ -886,7 +882,7 @@ public void assempleVariableScheduleFrom(final Loan loan, final String json) { if (isSkipRepaymentOnFirstMonthEnabled) { isSkipRepaymentOnFirstMonth = this.loanUtilService.isLoanRepaymentsSyncWithMeeting(loan.group(), loanCalendar); if (isSkipRepaymentOnFirstMonth) { - numberOfDays = configurationDomainService.retreivePeroidInNumberOfDaysForSkipMeetingDate().intValue(); + numberOfDays = configurationDomainService.retreivePeriodInNumberOfDaysForSkipMeetingDate().intValue(); } } final Integer minGap = installmentConfig.getMinimumGap(); @@ -1138,7 +1134,7 @@ private LocalDate deriveFirstRepaymentDateForLoans(final Integer repaymentEvery, final LocalDate refernceDateForCalculatingFirstRepaymentDate, final PeriodFrequencyType repaymentPeriodFrequencyType, final Integer minimumDaysBetweenDisbursalAndFirstRepayment, final Calendar calendar, final LocalDate submittedOnDate) { boolean isMeetingSkipOnFirstDayOfMonth = configurationDomainService.isSkippingMeetingOnFirstDayOfMonthEnabled(); - int numberOfDays = configurationDomainService.retreivePeroidInNumberOfDaysForSkipMeetingDate().intValue(); + int numberOfDays = configurationDomainService.retreivePeriodInNumberOfDaysForSkipMeetingDate().intValue(); final String frequency = CalendarUtils.getMeetingFrequencyFromPeriodFrequencyType(repaymentPeriodFrequencyType); final LocalDate derivedFirstRepayment = CalendarUtils.getFirstRepaymentMeetingDate(calendar, refernceDateForCalculatingFirstRepaymentDate, repaymentEvery, frequency, isMeetingSkipOnFirstDayOfMonth, numberOfDays); @@ -1384,4 +1380,70 @@ 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 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 d8d78ed21f0..5a596402160 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,12 @@ 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.LoanEvent; +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 +109,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 +193,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(); @@ -238,6 +254,7 @@ private void validateForCreate(final JsonElement element) { final Long groupId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.groupIdParameterName, element); final Client client = clientId != null ? this.clientRepository.findOneWithNotFoundDetection(clientId) : null; final Group group = groupId != null ? this.groupRepository.findOneWithNotFoundDetection(groupId) : null; + validateClientOrGroup(client, group, productId); validateOrThrow("loan", baseDataValidator -> { @@ -528,7 +545,7 @@ private void validateForCreate(final JsonElement element) { // charges loanChargeApiJsonValidator.validateLoanCharges(element, loanProduct, baseDataValidator); - /** + /* * TODO: Add collaterals for other loan accounts if needed. For now it's only applicable for individual * accounts. (loanType.isJLG() || loanType.isGLIM()) */ @@ -725,7 +742,7 @@ private void validateForCreate(final JsonElement element) { } checkForProductMixRestrictions(element); - validateSubmittedOnDate(element, null, loanProduct); + validateSubmittedOnDate(element, submittedOnDate, loanProduct); validateDisbursementDetails(loanProduct, element); validateCollateral(element); // validate if disbursement date is a holiday or a non-working day @@ -1381,7 +1398,6 @@ public void validateForModify(final JsonCommand command, final Loan loan) { validateDisbursementDetails(loanProduct, element); validateSubmittedOnDate(element, loan.getSubmittedOnDate(), loanProduct); - validateClientOrGroup(client, group, productId); // validate if disbursement date is a holiday or a non-working day @@ -1897,6 +1913,199 @@ 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); + } + + final LoanStatus newStatus = defaultLoanLifecycleStateMachine.dryTransition(LoanEvent.LOAN_APPROVED, loan); + if (newStatus.hasStateOf(loan.getStatus())) { + final String defaultUserMessage = "Loan is already approved."; + final ApiParameterError error = ApiParameterError + .generalError("error.msg.loan.approve.account.is.not.submitted.and.pending.state", defaultUserMessage); + baseDataValidator.getDataValidationErrors().add(error); + } + }); // 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 fae97c7952a..312854a2745 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 @@ -34,23 +34,18 @@ import java.util.List; import java.util.Locale; 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.fineract.infrastructure.configuration.domain.ConfigurationDomainService; +import org.apache.commons.lang3.tuple.Pair; import org.apache.fineract.infrastructure.core.api.JsonCommand; -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.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.dataqueries.data.EntityTables; import org.apache.fineract.infrastructure.dataqueries.data.StatusEnum; import org.apache.fineract.infrastructure.dataqueries.service.EntityDatatableChecksWritePlatformService; @@ -88,7 +83,6 @@ 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.exception.LoanApplicationNotInSubmittedAndPendingApprovalStateCannotBeDeleted; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleAssembler; @@ -113,7 +107,6 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements LoanApplicationWritePlatformService { private final PlatformSecurityContext context; - private final FromJsonHelper fromJsonHelper; private final LoanApplicationTransitionValidator loanApplicationTransitionValidator; private final LoanApplicationValidator loanApplicationValidator; private final LoanRepositoryWrapper loanRepositoryWrapper; @@ -125,9 +118,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa private final CalendarInstanceRepository calendarInstanceRepository; private final SavingsAccountRepositoryWrapper savingsAccountRepository; private final AccountAssociationsRepository accountAssociationsRepository; - private final LoanReadPlatformService loanReadPlatformService; private final BusinessEventNotifierService businessEventNotifierService; - private final ConfigurationDomainService configurationDomainService; private final LoanScheduleAssembler loanScheduleAssembler; private final LoanUtilService loanUtilService; private final CalendarReadPlatformService calendarReadPlatformService; @@ -513,19 +504,6 @@ public CommandProcessingResult deleteApplication(final Long loanId) { .build(); } - public void validateMultiDisbursementData(final JsonCommand command, LocalDate expectedDisbursementDate) { - final String json = command.json(); - final JsonElement element = this.fromJsonHelper.parse(json); - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan"); - final BigDecimal principal = this.fromJsonHelper.extractBigDecimalWithLocaleNamed("approvedLoanAmount", element); - loanApplicationValidator.validateLoanMultiDisbursementDate(element, baseDataValidator, expectedDisbursementDate, principal); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - @Transactional @Override public CommandProcessingResult approveGLIMLoanAppication(final Long loanId, final JsonCommand command) { @@ -575,100 +553,16 @@ public CommandProcessingResult approveGLIMLoanAppication(final Long loanId, fina @Transactional @Override public CommandProcessingResult approveApplication(final Long loanId, final JsonCommand command) { - final AppUser currentUser = getAppUserIfPresent(); - LocalDate expectedDisbursementDate = null; - - this.loanApplicationTransitionValidator.validateApproval(command.json()); - - Loan loan = retrieveLoanBy(loanId); - - final JsonArray disbursementDataArray = command.arrayOfParameterNamed(LoanApiConstants.disbursementDataParameterName); - - expectedDisbursementDate = command.localDateValueOfParameterNamed(LoanApiConstants.expectedDisbursementDateParameterName); - if (expectedDisbursementDate == null) { - expectedDisbursementDate = loan.getExpectedDisbursedOnLocalDate(); - } - if (loan.loanProduct().isMultiDisburseLoan()) { - this.validateMultiDisbursementData(command, expectedDisbursementDate); - } - - loanApplicationTransitionValidator.checkClientOrGroupActive(loan); - Boolean isSkipRepaymentOnFirstMonth = false; - Integer numberOfDays = 0; - // validate expected disbursement date against meeting date - if (loan.isSyncDisbursementWithMeeting() && (loan.isGroupLoan() || loan.isJLGLoan())) { - final CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByEntityId(loan.getId(), - CalendarEntityType.LOANS.getValue()); - Calendar calendar = null; - if (calendarInstance != null) { - calendar = calendarInstance.getCalendar(); - } - // final Calendar calendar = calendarInstance.getCalendar(); - boolean isSkipRepaymentOnFirstMonthEnabled = this.configurationDomainService.isSkippingMeetingOnFirstDayOfMonthEnabled(); - if (isSkipRepaymentOnFirstMonthEnabled) { - isSkipRepaymentOnFirstMonth = this.loanUtilService.isLoanRepaymentsSyncWithMeeting(loan.group(), calendar); - if (isSkipRepaymentOnFirstMonth) { - numberOfDays = configurationDomainService.retreivePeroidInNumberOfDaysForSkipMeetingDate().intValue(); - } - } - this.loanScheduleAssembler.validateDisbursementDateWithMeetingDates(expectedDisbursementDate, calendar, - isSkipRepaymentOnFirstMonth, numberOfDays); + loanApplicationValidator.validateApproval(command, loanId); - } - - 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 loan approved amount less than loan demanded amount, then need - // to recompute the schedule - if (changes.containsKey(LoanApiConstants.approvedLoanAmountParameterName) || changes.containsKey("recalculateLoanSchedule") - || changes.containsKey("expectedDisbursementDate")) { - LocalDate recalculateFrom = null; - ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); - loan.regenerateRepaymentSchedule(scheduleGeneratorDTO); - } - - if (loan.isTopup() && loan.getClientId() != null) { - final Long loanIdToClose = loan.getTopupLoanDetails().getLoanIdToClose(); - final Loan loanToClose = this.loanRepositoryWrapper.findNonClosedLoanThatBelongsToClient(loanIdToClose, loan.getClientId()); - if (loanToClose == null) { - throw new GeneralPlatformDomainRuleException("error.msg.loan.to.be.closed.with.topup.is.not.active", - "Loan to be closed with this topup is not active."); - } - - final LocalDate lastUserTransactionOnLoanToClose = loanToClose.getLastUserTransactionDate(); - if (DateUtils.isBefore(loan.getDisbursementDate(), lastUserTransactionOnLoanToClose)) { - throw new GeneralPlatformDomainRuleException( - "error.msg.loan.disbursal.date.should.be.after.last.transaction.date.of.loan.to.be.closed", - "Disbursal date of this loan application " + loan.getDisbursementDate() - + " should be after last transaction date of loan to be closed " + lastUserTransactionOnLoanToClose); - } - BigDecimal loanOutstanding = 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."); - } - BigDecimal netDisbursalAmount = loan.getApprovedPrincipal().subtract(loanOutstanding); - loan.adjustNetDisbursalAmount(netDisbursalAmount); - } - - loan = loanRepository.saveAndFlush(loan); - final String noteText = command.stringValueOfParameterNamed("note"); - if (StringUtils.isNotBlank(noteText)) { - final Note note = Note.loanNote(loan, noteText); - changes.put("note", noteText); - this.noteRepository.save(note); - } - + createNote(noteText, loan).ifPresent(note -> changes.put("note", noteText)); businessEventNotifierService.notifyPostBusinessEvent(new LoanApprovedBusinessEvent(loan)); } @@ -976,10 +870,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(); } } 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 614f8f24a1e..4699f41c8d3 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 @@ -108,7 +108,7 @@ public ScheduleGeneratorDTO buildScheduleGeneratorDTO(final Loan loan, final Loc if (isSkipRepaymentOnFirstMonthEnabled) { isSkipRepaymentOnFirstMonth = isLoanRepaymentsSyncWithMeeting(loan.group(), calendar); if (isSkipRepaymentOnFirstMonth) { - numberOfDays = configurationDomainService.retreivePeroidInNumberOfDaysForSkipMeetingDate().intValue(); + numberOfDays = configurationDomainService.retreivePeriodInNumberOfDaysForSkipMeetingDate().intValue(); } } final Boolean isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled = this.configurationDomainService @@ -240,7 +240,7 @@ private LocalDate calculateRepaymentStartingFromDate(final LocalDate actualDisbu boolean isSkipRepaymentOnFirstMonthEnabled = this.configurationDomainService .isSkippingMeetingOnFirstDayOfMonthEnabled(); if (isSkipRepaymentOnFirstMonthEnabled) { - numberOfDays = configurationDomainService.retreivePeroidInNumberOfDaysForSkipMeetingDate().intValue(); + numberOfDays = configurationDomainService.retreivePeriodInNumberOfDaysForSkipMeetingDate().intValue(); isSkipRepaymentOnFirstMonth = isLoanRepaymentsSyncWithMeeting(loan.group(), calendar); } calculatedRepaymentsStartingFromDate = CalendarUtils.getFirstRepaymentMeetingDate(calendar, actualDisbursementDate, @@ -268,7 +268,7 @@ private LocalDate generateCalculatedRepaymentStartDate(final CalendarHistoryData Integer numberOfDays = 0; boolean isSkipRepaymentOnFirstMonthEnabled = this.configurationDomainService.isSkippingMeetingOnFirstDayOfMonthEnabled(); if (isSkipRepaymentOnFirstMonthEnabled) { - numberOfDays = configurationDomainService.retreivePeroidInNumberOfDaysForSkipMeetingDate().intValue(); + numberOfDays = configurationDomainService.retreivePeriodInNumberOfDaysForSkipMeetingDate().intValue(); isSkipRepaymentOnFirstMonth = isLoanRepaymentsSyncWithMeeting(loan.group(), historyList.get(0).getCalendar()); } calculatedRepaymentsStartingFromDate = CalendarUtils.getNextRepaymentMeetingDate(historyList.get(0).getRecurrence(), 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 2743623a41f..55d485ed50c 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 @@ -2021,7 +2021,7 @@ public void applyMeetingDateChanges(final Calendar calendar, final Collection loanIDs = new ArrayList<>(); HashMap loanStatusHashMap; for (int i = 0; i < 3; i++) { - final Integer loanID = applyForLoanApplication(clientID.toString(), loanProductID.toString(), null, "10 January 2020"); + final Integer loanID = applyForLoanApplication(clientID.toString(), loanProductID.toString(), null, "1 March 2020"); Assertions.assertNotNull(loanID); @@ -946,7 +945,7 @@ public void testLoanCOBJobOutcomeWhileAddingFeeOnDisbursementDate() { final Integer loanProductID = createLoanProductWithPeriodicAccrual(null); Assertions.assertNotNull(loanProductID); - final Integer loanID = applyForLoanApplication(clientID.toString(), loanProductID.toString(), null, "10 January 2020"); + final Integer loanID = applyForLoanApplication(clientID.toString(), loanProductID.toString(), null, "1 June 2020"); Assertions.assertNotNull(loanID); @@ -1011,7 +1010,7 @@ public void testLoanCOBRunsOnlyOnLoansOneDayBehind() { Assertions.assertNotNull(loanProductID); HashMap loanStatusHashMap; - final Integer loanID = applyForLoanApplication(clientID.toString(), loanProductID.toString(), null, "10 January 2020"); + final Integer loanID = applyForLoanApplication(clientID.toString(), loanProductID.toString(), null, "1 July 2020"); Assertions.assertNotNull(loanID); @@ -1068,7 +1067,7 @@ public void testLoanCOBApplyPenaltyOnDue() { Assertions.assertNotNull(loanProductID); HashMap loanStatusHashMap; - final Integer loanID = applyForLoanApplication(clientID.toString(), loanProductID.toString(), null, "10 January 2019"); + final Integer loanID = applyForLoanApplication(clientID.toString(), loanProductID.toString(), null, "1 March 2019"); Assertions.assertNotNull(loanID); @@ -1129,7 +1128,7 @@ public void testLoanCOBApplyPenaltyOnDue1DayGracePeriod() { Assertions.assertNotNull(loanProductID); HashMap loanStatusHashMap; // Test penalty where there is 1 day grace period - final Integer loanID2 = applyForLoanApplication(clientID.toString(), loanProductID.toString(), null, "10 January 2020"); + final Integer loanID2 = applyForLoanApplication(clientID.toString(), loanProductID.toString(), null, "1 April 2020"); Assertions.assertNotNull(loanID2); @@ -1186,7 +1185,7 @@ public void testAvoidUnncessaryPenaltyWhenAmountZeroForOverdueLoansJobOutcome() final Integer loanProductID = createLoanProduct(overdueFeeChargeId.toString()); Assertions.assertNotNull(loanProductID); - final Integer loanID = applyForLoanApplication(clientID.toString(), loanProductID.toString(), null, "10 January 2013"); + final Integer loanID = applyForLoanApplication(clientID.toString(), loanProductID.toString(), null, "1 March 2013"); Assertions.assertNotNull(loanID); HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID); @@ -1238,7 +1237,7 @@ public void testUpdateOverdueDaysForNPA() throws InterruptedException { final Integer loanProductID = createLoanProduct(null); Assertions.assertNotNull(loanProductID); - final Integer loanID = applyForLoanApplication(clientID.toString(), loanProductID.toString(), null, "10 January 2013"); + final Integer loanID = applyForLoanApplication(clientID.toString(), loanProductID.toString(), null, "1 March 2013"); Assertions.assertNotNull(loanID); HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/InitiateExternalAssetOwnerTransferTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/InitiateExternalAssetOwnerTransferTest.java index 752217f4b49..dcab0859895 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/InitiateExternalAssetOwnerTransferTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/InitiateExternalAssetOwnerTransferTest.java @@ -1012,7 +1012,7 @@ private Integer createLoanForClient(Integer clientID) { Assertions.assertNotNull(loanProductID); HashMap loanStatusHashMap; - Integer loanID = applyForLoanApplication(clientID.toString(), loanProductID.toString(), "10 January 2020"); + Integer loanID = applyForLoanApplication(clientID.toString(), loanProductID.toString(), "1 March 2020"); Assertions.assertNotNull(loanID);