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..f8e960e39b8 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 @@ -42,7 +42,6 @@ import java.math.BigDecimal; import java.math.MathContext; import java.time.LocalDate; -import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Collection; @@ -1741,54 +1740,6 @@ private LocalDate determineExpectedMaturityDate() { return maturityDate; } - public Map loanApplicationWithdrawnByApplicant(final AppUser currentUser, final JsonCommand command, - final LoanLifecycleStateMachine loanLifecycleStateMachine) { - final Map actualChanges = new LinkedHashMap<>(); - - final LoanStatus statusEnum = loanLifecycleStateMachine.dryTransition(LoanEvent.LOAN_WITHDRAWN, this); - if (!statusEnum.hasStateOf(LoanStatus.fromInt(this.loanStatus))) { - loanLifecycleStateMachine.transition(LoanEvent.LOAN_WITHDRAWN, this); - actualChanges.put(PARAM_STATUS, LoanEnumerations.status(this.loanStatus)); - - LocalDate withdrawnOn = command.localDateValueOfParameterNamed(WITHDRAWN_ON_DATE); - if (withdrawnOn == null) { - withdrawnOn = command.localDateValueOfParameterNamed(EVENT_DATE); - } - - final Locale locale = new Locale(command.locale()); - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); - - this.withdrawnOnDate = withdrawnOn; - this.withdrawnBy = currentUser; - this.closedOnDate = withdrawnOn; - this.closedBy = currentUser; - - actualChanges.put(LOCALE, command.locale()); - actualChanges.put(DATE_FORMAT, command.dateFormat()); - actualChanges.put(WITHDRAWN_ON_DATE, withdrawnOn.format(fmt)); - actualChanges.put(CLOSED_ON_DATE, withdrawnOn.format(fmt)); - - if (DateUtils.isBefore(withdrawnOn, getSubmittedOnDate())) { - final String errorMessage = "The date on which a loan is withdrawn cannot be before its submittal date: " - + getSubmittedOnDate().toString(); - throw new InvalidLoanStateTransitionException("withdraw", "cannot.be.before.submittal.date", errorMessage, command, - getSubmittedOnDate()); - } - - validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_WITHDRAWN, withdrawnOn); - - if (DateUtils.isDateInTheFuture(withdrawnOn)) { - final String errorMessage = "The date on which a loan is withdrawn cannot be in the future."; - throw new InvalidLoanStateTransitionException("withdraw", "cannot.be.a.future.date", errorMessage, command); - } - } else { - final String errorMessage = "Only the loan applications with status 'Submitted and pending approval' are allowed to be withdrawn by applicant."; - throw new InvalidLoanStateTransitionException("withdraw", "cannot.withdraw", errorMessage); - } - - return actualChanges; - } - public Map loanApplicationApproval(final AppUser currentUser, final JsonCommand command, final JsonArray disbursementDataArray, final LoanLifecycleStateMachine loanLifecycleStateMachine) { validateAccountStatus(LoanEvent.LOAN_APPROVED); @@ -4278,11 +4229,6 @@ private void validateActivityNotBeforeClientOrGroupTransferDate(final LoanEvent action = "repayment.or.waiver"; postfix = "cannot.be.made.before.client.transfer.date"; } - case LOAN_WITHDRAWN -> { - errorMessage = "The date on which a loan is withdrawn cannot be earlier than client's transfer date to this office"; - action = "withdraw"; - postfix = "cannot.be.before.client.transfer.date"; - } case WRITE_OFF_OUTSTANDING -> { errorMessage = "The date on which a write off is made cannot be earlier than client's transfer date to this office"; action = "writeoff"; @@ -4478,14 +4424,6 @@ public void validateAccountStatus(final LoanEvent event) { dataValidationErrors.add(error); } } - case LOAN_WITHDRAWN -> { - if (!isSubmittedAndPendingApproval()) { - final String defaultUserMessage = "Loan application cannot be withdrawn. Loan Account is not in Submitted and Pending approval state."; - final ApiParameterError error = ApiParameterError - .generalError("error.msg.loan.withdrawn.account.is.not.submitted.pending.approval.state", defaultUserMessage); - dataValidationErrors.add(error); - } - } case WRITE_OFF_OUTSTANDING -> { if (!isOpen()) { final String defaultUserMessage = "Loan Written off is not allowed. Loan Account is not active."; 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..34d40c72784 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 @@ -29,6 +29,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.ApiParameterError; @@ -47,18 +48,14 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine; import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanStateTransitionException; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component +@RequiredArgsConstructor public final class LoanApplicationTransitionValidator { private final FromJsonHelper fromApiJsonHelper; - - @Autowired - public LoanApplicationTransitionValidator(final FromJsonHelper fromApiJsonHelper) { - this.fromApiJsonHelper = fromApiJsonHelper; - } + private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine; private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { if (!dataValidationErrors.isEmpty()) { @@ -112,13 +109,13 @@ public void validateApproval(final String json) { throwExceptionIfValidationWarningsExist(dataValidationErrors); } - public void validateRejection(final JsonCommand command, final Loan loan, final LoanLifecycleStateMachine loanLifecycleStateMachine) { + public void validateRejection(final JsonCommand command, final Loan loan) { // validate request body final String json = command.json(); validateLoanRejectionRequestBody(json); // validate loan rejection - validateLoanRejection(command, loan, loanLifecycleStateMachine); + validateLoanRejection(command, loan); } private void validateLoanRejectionRequestBody(String json) { @@ -146,8 +143,7 @@ private void validateLoanRejectionRequestBody(String json) { throwExceptionIfValidationWarningsExist(dataValidationErrors); } - private void validateLoanRejection(final JsonCommand command, final Loan loan, - final LoanLifecycleStateMachine loanLifecycleStateMachine) { + private void validateLoanRejection(final JsonCommand command, final Loan loan) { // validate client or group is active checkClientOrGroupActive(loan); @@ -156,7 +152,7 @@ private void validateLoanRejection(final JsonCommand command, final Loan loan, // validate loan state transition - final LoanStatus statusEnum = loanLifecycleStateMachine.dryTransition(LoanEvent.LOAN_REJECTED, loan); + final LoanStatus statusEnum = defaultLoanLifecycleStateMachine.dryTransition(LoanEvent.LOAN_REJECTED, loan); if (!statusEnum.hasStateOf(LoanStatus.fromInt(loan.getLoanStatus()))) { final LocalDate rejectedOn = command.localDateValueOfParameterNamed(Loan.REJECTED_ON_DATE); if (DateUtils.isBefore(rejectedOn, loan.getSubmittedOnDate())) { @@ -178,8 +174,16 @@ private void validateLoanRejection(final JsonCommand command, final Loan loan, } } - public void validateApplicantWithdrawal(final String json) { + public void validateApplicantWithdrawal(final JsonCommand command, final Loan loan) { + // validate request body + final String json = command.json(); + validateApplicantWithdrawalRequestBody(json); + // validate Loan application withdrawal by applicant + validateLoanApplicantWithdrawal(command, loan); + } + + private void validateApplicantWithdrawalRequestBody(String json) { if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); } @@ -204,6 +208,37 @@ public void validateApplicantWithdrawal(final String json) { throwExceptionIfValidationWarningsExist(dataValidationErrors); } + private void validateLoanApplicantWithdrawal(final JsonCommand command, final Loan loan) { + // validate client or group is active + checkClientOrGroupActive(loan); + + // validate loan state transition + final LoanStatus statusEnum = defaultLoanLifecycleStateMachine.dryTransition(LoanEvent.LOAN_WITHDRAWN, loan); + if (!statusEnum.hasStateOf(LoanStatus.fromInt(loan.getLoanStatus()))) { + LocalDate withdrawnOn = command.localDateValueOfParameterNamed(Loan.WITHDRAWN_ON_DATE); + if (withdrawnOn == null) { + withdrawnOn = command.localDateValueOfParameterNamed(Loan.EVENT_DATE); + } + if (DateUtils.isBefore(withdrawnOn, loan.getSubmittedOnDate())) { + final String errorMessage = "The date on which a loan is withdrawn cannot be before its submittal date: " + + loan.getSubmittedOnDate().toString(); + throw new InvalidLoanStateTransitionException("withdraw", "cannot.be.before.submittal.date", errorMessage, command, + loan.getSubmittedOnDate()); + } + + validateActivityNotBeforeClientOrGroupTransferDate(loan, LoanEvent.LOAN_WITHDRAWN, withdrawnOn); + + if (DateUtils.isDateInTheFuture(withdrawnOn)) { + final String errorMessage = "The date on which a loan is withdrawn cannot be in the future."; + throw new InvalidLoanStateTransitionException("withdraw", "cannot.be.a.future.date", errorMessage, command); + } + } else { + final String errorMessage = "Only the loan applications with status 'Submitted and pending approval' are allowed to be withdrawn by applicant."; + throw new InvalidLoanStateTransitionException("withdraw", "cannot.withdraw", errorMessage); + } + + } + private void validateActivityNotBeforeClientOrGroupTransferDate(final Loan loan, final LoanEvent event, final LocalDate activityDate) { if (loan.getClient() != null && loan.getClient().getOfficeJoiningDate() != null) { final LocalDate clientOfficeJoiningDate = loan.getClient().getOfficeJoiningDate(); @@ -217,6 +252,11 @@ private void validateActivityNotBeforeClientOrGroupTransferDate(final Loan loan, action = "reject"; postfix = "cannot.be.before.client.transfer.date"; } + case LOAN_WITHDRAWN -> { + errorMessage = "The date on which a loan is withdrawn cannot be earlier than client's transfer date to this office"; + action = "withdraw"; + postfix = "cannot.be.before.client.transfer.date"; + } default -> { } } 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..94aad8ec9c9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java @@ -26,13 +26,10 @@ import jakarta.persistence.PersistenceException; import java.math.BigDecimal; import java.time.LocalDate; -import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoField; import java.util.ArrayList; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Set; import lombok.RequiredArgsConstructor; @@ -96,7 +93,6 @@ import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationValidator; import org.apache.fineract.portfolio.loanproduct.LoanProductConstants; import org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType; -import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations; import org.apache.fineract.portfolio.note.domain.Note; import org.apache.fineract.portfolio.note.domain.NoteRepository; import org.apache.fineract.portfolio.savings.data.GroupSavingsIndividualMonitoringAccountData; @@ -492,6 +488,7 @@ public CommandProcessingResult deleteApplication(final Long loanId) { this.accountAssociationsRepository.delete(accountAssociations); } + // Note: check if releaseAttachedCollaterals method can be used here Set loanCollateralManagements = loan.getLoanCollateralManagements(); for (LoanCollateralManagement loanCollateralManagement : loanCollateralManagements) { BigDecimal quantity = loanCollateralManagement.getQuantity(); @@ -797,20 +794,23 @@ public CommandProcessingResult rejectApplication(final Long loanId, final JsonCo Loan loan = retrieveLoanBy(loanId); // validate loan rejection - loanApplicationTransitionValidator.validateRejection(command, loan, defaultLoanLifecycleStateMachine); + loanApplicationTransitionValidator.validateRejection(command, loan); // check for mandatory entities entityDatatableChecksWritePlatformService.runTheCheckForProduct(loanId, EntityTables.LOAN.getName(), StatusEnum.REJECTED.getCode().longValue(), EntityTables.LOAN.getForeignKeyColumnNameOnDatatable(), loan.productId()); // loan application rejection - final Map changes = loanApplicationRejection(loan, command); + final AppUser currentUser = getAppUserIfPresent(); + defaultLoanLifecycleStateMachine.transition(LoanEvent.LOAN_REJECTED, loan); + final Map changes = loanAssembler.updateLoanApplicationAttributesForRejection(loan, command, currentUser); if (!changes.isEmpty()) { loanRepositoryWrapper.saveAndFlush(loan); final String noteText = command.stringValueOfParameterNamed("note"); createNote(noteText, loan); } + businessEventNotifierService.notifyPostBusinessEvent(new LoanRejectedBusinessEvent(loan)); return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // @@ -824,63 +824,31 @@ public CommandProcessingResult rejectApplication(final Long loanId, final JsonCo .build(); } - private Map loanApplicationRejection(Loan loan, final JsonCommand command) { - - final AppUser currentUser = getAppUserIfPresent(); - final LocalDate rejectedOn = command.localDateValueOfParameterNamed(Loan.REJECTED_ON_DATE); - - loan.setRejectedOnDate(rejectedOn); - loan.setRejectedBy(currentUser); - loan.setClosedOnDate(rejectedOn); - loan.setClosedBy(currentUser); - - defaultLoanLifecycleStateMachine.transition(LoanEvent.LOAN_REJECTED, loan); - - final Map actualChanges = new LinkedHashMap<>(); - final Locale locale = new Locale(command.locale()); - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); - - actualChanges.put(Loan.PARAM_STATUS, LoanEnumerations.status(loan.getStatus())); - actualChanges.put(Loan.LOCALE, command.locale()); - actualChanges.put(Loan.DATE_FORMAT, command.dateFormat()); - actualChanges.put(Loan.REJECTED_ON_DATE, rejectedOn.format(fmt)); - actualChanges.put(Loan.CLOSED_ON_DATE, rejectedOn.format(fmt)); - - return actualChanges; - } - @Transactional @Override public CommandProcessingResult applicantWithdrawsFromApplication(final Long loanId, final JsonCommand command) { - final AppUser currentUser = getAppUserIfPresent(); - - this.loanApplicationTransitionValidator.validateApplicantWithdrawal(command.json()); - + // retrieve loan final Loan loan = retrieveLoanBy(loanId); - loanApplicationTransitionValidator.checkClientOrGroupActive(loan); + // validate withdrawal + loanApplicationTransitionValidator.validateApplicantWithdrawal(command, loan); + + // check for mandatory entities entityDatatableChecksWritePlatformService.runTheCheckForProduct(loanId, EntityTables.LOAN.getName(), StatusEnum.WITHDRAWN.getCode().longValue(), EntityTables.LOAN.getForeignKeyColumnNameOnDatatable(), loan.productId()); - final Map changes = loan.loanApplicationWithdrawnByApplicant(currentUser, command, - defaultLoanLifecycleStateMachine); - + // loan application withdrawal + final AppUser currentUser = getAppUserIfPresent(); + defaultLoanLifecycleStateMachine.transition(LoanEvent.LOAN_WITHDRAWN, loan); + final Map changes = loanAssembler.updateLoanApplicationAttributesForWithdrawal(loan, command, currentUser); // Release attached collaterals if (loan.getLoanType().isIndividualAccount()) { - Set loanCollateralManagements = loan.getLoanCollateralManagements(); - for (LoanCollateralManagement loanCollateralManagement : loanCollateralManagements) { - ClientCollateralManagement clientCollateralManagement = loanCollateralManagement.getClientCollateralManagement(); - clientCollateralManagement - .updateQuantity(clientCollateralManagement.getQuantity().add(loanCollateralManagement.getQuantity())); - loanCollateralManagement.setClientCollateralManagement(clientCollateralManagement); - loanCollateralManagement.setIsReleased(true); - } - loan.updateLoanCollateral(loanCollateralManagements); + releaseAttachedCollaterals(loan); } if (!changes.isEmpty()) { - this.loanRepositoryWrapper.saveAndFlush(loan); + loanRepositoryWrapper.saveAndFlush(loan); final String noteText = command.stringValueOfParameterNamed("note"); createNote(noteText, loan); @@ -983,4 +951,15 @@ private void createNote(String submittedOnNote, Loan newLoanApplication) { } } + private void releaseAttachedCollaterals(Loan loan) { + Set loanCollateralManagements = loan.getLoanCollateralManagements(); + for (LoanCollateralManagement loanCollateralManagement : loanCollateralManagements) { + ClientCollateralManagement clientCollateralManagement = loanCollateralManagement.getClientCollateralManagement(); + clientCollateralManagement.updateQuantity(clientCollateralManagement.getQuantity().add(loanCollateralManagement.getQuantity())); + loanCollateralManagement.setClientCollateralManagement(clientCollateralManagement); + loanCollateralManagement.setIsReleased(true); + } + loan.updateLoanCollateral(loanCollateralManagements); + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java index a62e25b8cac..ebfce389e59 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java @@ -22,9 +22,11 @@ import com.google.gson.JsonElement; import java.math.BigDecimal; import java.time.LocalDate; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -96,8 +98,10 @@ import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository; import org.apache.fineract.portfolio.loanproduct.exception.LoanProductNotFoundException; +import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations; import org.apache.fineract.portfolio.rate.domain.Rate; import org.apache.fineract.portfolio.rate.service.RateAssembler; +import org.apache.fineract.useradministration.domain.AppUser; @RequiredArgsConstructor public class LoanAssembler { @@ -833,4 +837,49 @@ public Map updateFrom(JsonCommand command, Loan loan) { return changes; } + + public Map updateLoanApplicationAttributesForWithdrawal(Loan loan, JsonCommand command, AppUser currentUser) { + final Map actualChanges = new LinkedHashMap<>(); + + LocalDate withdrawnOn = command.localDateValueOfParameterNamed(Loan.WITHDRAWN_ON_DATE); + if (withdrawnOn == null) { + withdrawnOn = command.localDateValueOfParameterNamed(Loan.EVENT_DATE); + } + + loan.setWithdrawnOnDate(withdrawnOn); + loan.setWithdrawnBy(currentUser); + loan.setClosedOnDate(withdrawnOn); + loan.setClosedBy(currentUser); + + final Locale locale = new Locale(command.locale()); + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); + + actualChanges.put(Loan.PARAM_STATUS, LoanEnumerations.status(loan.getStatus())); + actualChanges.put(Loan.LOCALE, command.locale()); + actualChanges.put(Loan.DATE_FORMAT, command.dateFormat()); + actualChanges.put(Loan.WITHDRAWN_ON_DATE, withdrawnOn.format(fmt)); + actualChanges.put(Loan.CLOSED_ON_DATE, withdrawnOn.format(fmt)); + return actualChanges; + } + + public Map updateLoanApplicationAttributesForRejection(Loan loan, JsonCommand command, AppUser currentUser) { + final Map actualChanges = new LinkedHashMap<>(); + + final LocalDate rejectedOn = command.localDateValueOfParameterNamed(Loan.REJECTED_ON_DATE); + + loan.setRejectedOnDate(rejectedOn); + loan.setRejectedBy(currentUser); + loan.setClosedOnDate(rejectedOn); + loan.setClosedBy(currentUser); + + final Locale locale = new Locale(command.locale()); + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); + + actualChanges.put(Loan.PARAM_STATUS, LoanEnumerations.status(loan.getStatus())); + actualChanges.put(Loan.LOCALE, command.locale()); + actualChanges.put(Loan.DATE_FORMAT, command.dateFormat()); + actualChanges.put(Loan.REJECTED_ON_DATE, rejectedOn.format(fmt)); + actualChanges.put(Loan.CLOSED_ON_DATE, rejectedOn.format(fmt)); + return actualChanges; + } }