Skip to content

Commit

Permalink
FINERACT-2090: Restructure Withdraw Loan Application Logic
Browse files Browse the repository at this point in the history
  • Loading branch information
ruchiD authored and adamsaghy committed Jun 20, 2024
1 parent 0292171 commit 1c53fe2
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 123 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1741,54 +1740,6 @@ private LocalDate determineExpectedMaturityDate() {
return maturityDate;
}

public Map<String, Object> loanApplicationWithdrawnByApplicant(final AppUser currentUser, final JsonCommand command,
final LoanLifecycleStateMachine loanLifecycleStateMachine) {
final Map<String, Object> 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<String, Object> loanApplicationApproval(final AppUser currentUser, final JsonCommand command,
final JsonArray disbursementDataArray, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
validateAccountStatus(LoanEvent.LOAN_APPROVED);
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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.";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<ApiParameterError> dataValidationErrors) {
if (!dataValidationErrors.isEmpty()) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);

Expand All @@ -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())) {
Expand All @@ -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();
}
Expand All @@ -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();
Expand All @@ -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 -> {
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -492,6 +488,7 @@ public CommandProcessingResult deleteApplication(final Long loanId) {
this.accountAssociationsRepository.delete(accountAssociations);
}

// Note: check if releaseAttachedCollaterals method can be used here
Set<LoanCollateralManagement> loanCollateralManagements = loan.getLoanCollateralManagements();
for (LoanCollateralManagement loanCollateralManagement : loanCollateralManagements) {
BigDecimal quantity = loanCollateralManagement.getQuantity();
Expand Down Expand Up @@ -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<String, Object> changes = loanApplicationRejection(loan, command);
final AppUser currentUser = getAppUserIfPresent();
defaultLoanLifecycleStateMachine.transition(LoanEvent.LOAN_REJECTED, loan);
final Map<String, Object> 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()) //
Expand All @@ -824,63 +824,31 @@ public CommandProcessingResult rejectApplication(final Long loanId, final JsonCo
.build();
}

private Map<String, Object> 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<String, Object> 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<String, Object> changes = loan.loanApplicationWithdrawnByApplicant(currentUser, command,
defaultLoanLifecycleStateMachine);

// loan application withdrawal
final AppUser currentUser = getAppUserIfPresent();
defaultLoanLifecycleStateMachine.transition(LoanEvent.LOAN_WITHDRAWN, loan);
final Map<String, Object> changes = loanAssembler.updateLoanApplicationAttributesForWithdrawal(loan, command, currentUser);
// Release attached collaterals
if (loan.getLoanType().isIndividualAccount()) {
Set<LoanCollateralManagement> 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);
Expand Down Expand Up @@ -983,4 +951,15 @@ private void createNote(String submittedOnNote, Loan newLoanApplication) {
}
}

private void releaseAttachedCollaterals(Loan loan) {
Set<LoanCollateralManagement> 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);
}

}
Loading

0 comments on commit 1c53fe2

Please sign in to comment.