Skip to content

Commit

Permalink
FINERACT-2090: Improve Loan Application Reject logic
Browse files Browse the repository at this point in the history
  • Loading branch information
ruchiD committed Jun 14, 2024
1 parent f48f94c commit 406d512
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1743,47 +1743,26 @@ private LocalDate determineExpectedMaturityDate() {

public Map<String, Object> loanApplicationRejection(final AppUser currentUser, final JsonCommand command,
final LoanLifecycleStateMachine loanLifecycleStateMachine) {
validateAccountStatus(LoanEvent.LOAN_REJECTED);

final Map<String, Object> actualChanges = new LinkedHashMap<>();

final LoanStatus statusEnum = loanLifecycleStateMachine.dryTransition(LoanEvent.LOAN_REJECTED, this);
if (!statusEnum.hasStateOf(LoanStatus.fromInt(this.loanStatus))) {
final LocalDate rejectedOn = command.localDateValueOfParameterNamed(REJECTED_ON_DATE);

final Locale locale = new Locale(command.locale());
final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale);

this.rejectedOnDate = rejectedOn;
this.rejectedBy = currentUser;
this.closedOnDate = rejectedOn;
this.closedBy = currentUser;

loanLifecycleStateMachine.transition(LoanEvent.LOAN_REJECTED, this);
actualChanges.put(PARAM_STATUS, LoanEnumerations.status(this.loanStatus));
final LocalDate rejectedOn = command.localDateValueOfParameterNamed(REJECTED_ON_DATE);

actualChanges.put(LOCALE, command.locale());
actualChanges.put(DATE_FORMAT, command.dateFormat());
actualChanges.put(REJECTED_ON_DATE, rejectedOn.format(fmt));
actualChanges.put(CLOSED_ON_DATE, rejectedOn.format(fmt));
final Locale locale = new Locale(command.locale());
final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale);

if (DateUtils.isBefore(rejectedOn, getSubmittedOnDate())) {
final String errorMessage = "The date on which a loan is rejected cannot be before its submittal date: "
+ getSubmittedOnDate().toString();
throw new InvalidLoanStateTransitionException("reject", "cannot.be.before.submittal.date", errorMessage, rejectedOn,
getSubmittedOnDate());
}
this.rejectedOnDate = rejectedOn;
this.rejectedBy = currentUser;
this.closedOnDate = rejectedOn;
this.closedBy = currentUser;

validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_REJECTED, rejectedOn);
loanLifecycleStateMachine.transition(LoanEvent.LOAN_REJECTED, this);
actualChanges.put(PARAM_STATUS, LoanEnumerations.status(this.loanStatus));

if (DateUtils.isDateInTheFuture(rejectedOn)) {
final String errorMessage = "The date on which a loan is rejected cannot be in the future.";
throw new InvalidLoanStateTransitionException("reject", "cannot.be.a.future.date", errorMessage, rejectedOn);
}
} else {
final String errorMessage = "Only the loan applications with status 'Submitted and pending approval' are allowed to be rejected.";
throw new InvalidLoanStateTransitionException("reject", "cannot.reject", errorMessage);
}
actualChanges.put(LOCALE, command.locale());
actualChanges.put(DATE_FORMAT, command.dateFormat());
actualChanges.put(REJECTED_ON_DATE, rejectedOn.format(fmt));
actualChanges.put(CLOSED_ON_DATE, rejectedOn.format(fmt));

return actualChanges;
}
Expand Down Expand Up @@ -3559,7 +3538,7 @@ public boolean isApproved() {
return getStatus().isApproved();
}

private boolean isNotDisbursed() {
public boolean isNotDisbursed() {
return !isDisbursed();
}

Expand Down Expand Up @@ -3603,7 +3582,7 @@ public boolean isOpen() {
return getStatus().isActive();
}

private boolean isAllTranchesNotDisbursed() {
public boolean isAllTranchesNotDisbursed() {
LoanStatus actualLoanStatus = LoanStatus.fromInt(this.loanStatus);
return this.loanProduct.isMultiDisburseLoan() && (actualLoanStatus.isActive() || actualLoanStatus.isApproved()
|| actualLoanStatus.isClosedObligationsMet() || actualLoanStatus.isOverpaid()) && isDisbursementAllowed();
Expand Down Expand Up @@ -4325,11 +4304,6 @@ private void validateActivityNotBeforeClientOrGroupTransferDate(final LoanEvent
action = "repayment.or.waiver";
postfix = "cannot.be.made.before.client.transfer.date";
}
case LOAN_REJECTED -> {
errorMessage = "The date on which a loan is rejected cannot be earlier than client's transfer date to this office";
action = "reject";
postfix = "cannot.be.before.client.transfer.date";
}
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";
Expand Down Expand Up @@ -4530,14 +4504,6 @@ public void validateAccountStatus(final LoanEvent event) {
dataValidationErrors.add(error);
}
}
case LOAN_REJECTED -> {
if (!isSubmittedAndPendingApproval()) {
final String defaultUserMessage = "Loan application cannot be rejected. Loan Account is not in Submitted and Pending approval state.";
final ApiParameterError error = ApiParameterError
.generalError("error.msg.loan.reject.account.is.not.submitted.pending.approval.state", defaultUserMessage);
dataValidationErrors.add(error);
}
}
case LOAN_WITHDRAWN -> {
if (!isSubmittedAndPendingApproval()) {
final String defaultUserMessage = "Loan application cannot be withdrawn. Loan Account is not in Submitted and Pending approval state.";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,22 +30,29 @@
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent;
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
public final class LoanApplicationTransitionApiJsonValidator {
public final class LoanApplicationTransitionValidator {

private final FromJsonHelper fromApiJsonHelper;

@Autowired
public LoanApplicationTransitionApiJsonValidator(final FromJsonHelper fromApiJsonHelper) {
public LoanApplicationTransitionValidator(final FromJsonHelper fromApiJsonHelper) {
this.fromApiJsonHelper = fromApiJsonHelper;
}

Expand Down Expand Up @@ -152,4 +159,72 @@ public void validateApplicantWithdrawal(final String json) {

throwExceptionIfValidationWarningsExist(dataValidationErrors);
}

public void validateRejection(final Loan loan, final LoanLifecycleStateMachine loanLifecycleStateMachine, JsonCommand command) {
validateAccountStatus(loan, LoanEvent.LOAN_REJECTED);
final LoanStatus statusEnum = loanLifecycleStateMachine.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())) {
final String errorMessage = "The date on which a loan is rejected cannot be before its submittal date: "
+ loan.getSubmittedOnDate().toString();
throw new InvalidLoanStateTransitionException("reject", "cannot.be.before.submittal.date", errorMessage, rejectedOn,
loan.getSubmittedOnDate());
}

validateActivityNotBeforeClientOrGroupTransferDate(loan, LoanEvent.LOAN_REJECTED, rejectedOn);

if (DateUtils.isDateInTheFuture(rejectedOn)) {
final String errorMessage = "The date on which a loan is rejected cannot be in the future.";
throw new InvalidLoanStateTransitionException("reject", "cannot.be.a.future.date", errorMessage, rejectedOn);
}
} else {
final String errorMessage = "Only the loan applications with status 'Submitted and pending approval' are allowed to be rejected.";
throw new InvalidLoanStateTransitionException("reject", "cannot.reject", 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();
if (DateUtils.isBefore(activityDate, clientOfficeJoiningDate)) {
String errorMessage = null;
String action = null;
String postfix = null;
switch (event) {
case LOAN_REJECTED -> {
errorMessage = "The date on which a loan is rejected cannot be earlier than client's transfer date to this office";
action = "reject";
postfix = "cannot.be.before.client.transfer.date";
}
default -> {
}
}
throw new InvalidLoanStateTransitionException(action, postfix, errorMessage, clientOfficeJoiningDate);
}
}
}

public void validateAccountStatus(final Loan loan, final LoanEvent event) {
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();

switch (event) {
case LOAN_REJECTED -> {
if (!loan.isSubmittedAndPendingApproval()) {
final String defaultUserMessage = "Loan application cannot be rejected. Loan Account is not in Submitted and Pending approval state.";
final ApiParameterError error = ApiParameterError
.generalError("error.msg.loan.reject.account.is.not.submitted.pending.approval.state", defaultUserMessage);
dataValidationErrors.add(error);
}
}
default -> {
}
}

if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
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;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationTransitionApiJsonValidator;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationTransitionValidator;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationValidator;
import org.apache.fineract.portfolio.loanproduct.LoanProductConstants;
import org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType;
Expand All @@ -113,7 +113,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa

private final PlatformSecurityContext context;
private final FromJsonHelper fromJsonHelper;
private final LoanApplicationTransitionApiJsonValidator loanApplicationTransitionApiJsonValidator;
private final LoanApplicationTransitionValidator loanApplicationTransitionValidator;
private final LoanApplicationValidator loanApplicationValidator;
private final LoanRepositoryWrapper loanRepositoryWrapper;
private final NoteRepository noteRepository;
Expand Down Expand Up @@ -578,7 +578,7 @@ public CommandProcessingResult approveApplication(final Long loanId, final JsonC
final AppUser currentUser = getAppUserIfPresent();
LocalDate expectedDisbursementDate = null;

this.loanApplicationTransitionApiJsonValidator.validateApproval(command.json());
this.loanApplicationTransitionValidator.validateApproval(command.json());

Loan loan = retrieveLoanBy(loanId);

Expand Down Expand Up @@ -794,19 +794,25 @@ public CommandProcessingResult rejectApplication(final Long loanId, final JsonCo

final AppUser currentUser = getAppUserIfPresent();

this.loanApplicationTransitionApiJsonValidator.validateRejection(command.json());
// validate before loan assembling
loanApplicationTransitionValidator.validateRejection(command.json());

// retrieve/assemble loan
final Loan loan = retrieveLoanBy(loanId);

// check client or group
checkClientOrGroupActive(loan);

// check for mandatory entities
entityDatatableChecksWritePlatformService.runTheCheckForProduct(loanId, EntityTables.LOAN.getName(),
StatusEnum.REJECTED.getCode().longValue(), EntityTables.LOAN.getForeignKeyColumnNameOnDatatable(), loan.productId());

// validate loan rejection
loanApplicationTransitionValidator.validateRejection(loan, defaultLoanLifecycleStateMachine, command);

final Map<String, Object> changes = loan.loanApplicationRejection(currentUser, command, defaultLoanLifecycleStateMachine);
if (!changes.isEmpty()) {
this.loanRepositoryWrapper.saveAndFlush(loan);

loanRepositoryWrapper.saveAndFlush(loan);
final String noteText = command.stringValueOfParameterNamed("note");
createNote(noteText, loan);
}
Expand All @@ -829,7 +835,7 @@ public CommandProcessingResult applicantWithdrawsFromApplication(final Long loan

final AppUser currentUser = getAppUserIfPresent();

this.loanApplicationTransitionApiJsonValidator.validateApplicantWithdrawal(command.json());
this.loanApplicationTransitionValidator.validateApplicantWithdrawal(command.json());

final Loan loan = retrieveLoanBy(loanId);
checkClientOrGroupActive(loan);
Expand Down Expand Up @@ -880,16 +886,12 @@ private Loan retrieveLoanBy(final Long loanId) {

private void checkClientOrGroupActive(final Loan loan) {
final Client client = loan.client();
if (client != null) {
if (client.isNotActive()) {
throw new ClientNotActiveException(client.getId());
}
if (client != null && client.isNotActive()) {
throw new ClientNotActiveException(client.getId());
}
final Group group = loan.group();
if (group != null) {
if (group.isNotActive()) {
throw new GroupNotActiveException(group.getId());
}
if (group != null && group.isNotActive()) {
throw new GroupNotActiveException(group.getId());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
import org.apache.fineract.portfolio.loanaccount.mapper.LoanChargeMapper;
import org.apache.fineract.portfolio.loanaccount.mapper.LoanCollateralManagementMapper;
import org.apache.fineract.portfolio.loanaccount.mapper.LoanTransactionRelationMapper;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationTransitionApiJsonValidator;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationTransitionValidator;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationValidator;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeApiJsonValidator;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanEventApiJsonValidator;
Expand Down Expand Up @@ -202,7 +202,7 @@ public LoanAccrualWritePlatformService loanAccrualWritePlatformService(LoanReadP
@Bean
@ConditionalOnMissingBean(LoanApplicationWritePlatformService.class)
public LoanApplicationWritePlatformService loanApplicationWritePlatformService(PlatformSecurityContext context,
FromJsonHelper fromJsonHelper, LoanApplicationTransitionApiJsonValidator loanApplicationTransitionApiJsonValidator,
FromJsonHelper fromJsonHelper, LoanApplicationTransitionValidator loanApplicationTransitionValidator,
LoanApplicationValidator loanApplicationValidator, LoanRepositoryWrapper loanRepositoryWrapper, NoteRepository noteRepository,
LoanAssembler loanAssembler, LoanSummaryWrapper loanSummaryWrapper,
LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory,
Expand All @@ -214,7 +214,7 @@ public LoanApplicationWritePlatformService loanApplicationWritePlatformService(P
EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService, GLIMAccountInfoRepository glimRepository,
LoanRepository loanRepository, GSIMReadPlatformService gsimReadPlatformService,
LoanLifecycleStateMachine defaultLoanLifecycleStateMachine) {
return new LoanApplicationWritePlatformServiceJpaRepositoryImpl(context, fromJsonHelper, loanApplicationTransitionApiJsonValidator,
return new LoanApplicationWritePlatformServiceJpaRepositoryImpl(context, fromJsonHelper, loanApplicationTransitionValidator,
loanApplicationValidator, loanRepositoryWrapper, noteRepository, loanAssembler, loanSummaryWrapper,
loanRepaymentScheduleTransactionProcessorFactory, calendarRepository, calendarInstanceRepository, savingsAccountRepository,
accountAssociationsRepository, loanReadPlatformService, businessEventNotifierService, configurationDomainService,
Expand Down

0 comments on commit 406d512

Please sign in to comment.