Skip to content

Commit

Permalink
cleanup approveApplication
Browse files Browse the repository at this point in the history
  • Loading branch information
kjozsa committed Jun 14, 2024
1 parent 8bfbc96 commit 9b988e0
Show file tree
Hide file tree
Showing 5 changed files with 304 additions and 248 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1836,114 +1836,11 @@ public Map<String, Object> loanApplicationWithdrawnByApplicant(final AppUser cur
return actualChanges;
}

public Map<String, Object> loanApplicationApproval(final AppUser currentUser, final JsonCommand command,
final JsonArray disbursementDataArray, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
validateAccountStatus(LoanEvent.LOAN_APPROVED);

final Map<String, Object> 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<LoanDisbursementDetails> 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<LoanDisbursementDetails> getLoanDisbursementDetails() {
public List<LoanDisbursementDetails> getLoanDisbursementDetails() {
List<LoanDisbursementDetails> currentDisbursementDetails = getDisbursementDetails();
if (loanProduct.isDisallowExpectedDisbursements()) {
if (!currentDisbursementDetails.isEmpty()) {
Expand All @@ -1959,24 +1856,7 @@ private List<LoanDisbursementDetails> 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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,50 +56,6 @@ private void throwExceptionIfValidationWarningsExist(final List<ApiParameterErro
}
}

public void validateApproval(final String json) {

if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}

final Set<String> 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<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, disbursementParameters);

final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loanapplication");

final JsonElement element = this.fromApiJsonHelper.parse(json);

final BigDecimal principal = this.fromApiJsonHelper
.extractBigDecimalWithLocaleNamed(LoanApiConstants.approvedLoanAmountParameterName, element);
baseDataValidator.reset().parameter(LoanApiConstants.approvedLoanAmountParameterName).value(principal).ignoreIfNull()
.positiveAmount();

final BigDecimal netDisbursalAmount = this.fromApiJsonHelper
.extractBigDecimalWithLocaleNamed(LoanApiConstants.disbursementNetDisbursalAmountParameterName, element);
baseDataValidator.reset().parameter(LoanApiConstants.disbursementNetDisbursalAmountParameterName).value(netDisbursalAmount)
.ignoreIfNull().positiveAmount();

final LocalDate approvedOnDate = this.fromApiJsonHelper.extractLocalDateNamed(LoanApiConstants.approvedOnDateParameterName,
element);
baseDataValidator.reset().parameter(LoanApiConstants.approvedOnDateParameterName).value(approvedOnDate).notNull();

final LocalDate expectedDisbursementDate = this.fromApiJsonHelper
.extractLocalDateNamed(LoanApiConstants.expectedDisbursementDateParameterName, element);
baseDataValidator.reset().parameter(LoanApiConstants.expectedDisbursementDateParameterName).value(expectedDisbursementDate)
.ignoreIfNull();

final String note = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.noteParameterName, element);
baseDataValidator.reset().parameter(LoanApiConstants.noteParameterName).value(note).notExceedingLengthOf(1000);

throwExceptionIfValidationWarningsExist(dataValidationErrors);
}

public void validateRejection(final String json) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import java.util.TreeSet;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
Expand Down Expand Up @@ -87,7 +88,12 @@
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails;
import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent;
import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine;
import org.apache.fineract.portfolio.loanaccount.domain.LoanOfficerAssignmentHistory;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
Expand Down Expand Up @@ -118,8 +124,16 @@
import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType;
import org.apache.fineract.portfolio.loanproduct.exception.LoanProductNotFoundException;
import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.stereotype.Service;

import static org.apache.fineract.portfolio.loanaccount.domain.Loan.APPROVED_ON_DATE;
import static org.apache.fineract.portfolio.loanaccount.domain.Loan.DATE_FORMAT;
import static org.apache.fineract.portfolio.loanaccount.domain.Loan.EVENT_DATE;
import static org.apache.fineract.portfolio.loanaccount.domain.Loan.EXPECTED_DISBURSEMENT_DATE;
import static org.apache.fineract.portfolio.loanaccount.domain.Loan.LOCALE;
import static org.apache.fineract.portfolio.loanaccount.domain.Loan.PARAM_STATUS;

@Service
@RequiredArgsConstructor
public class LoanScheduleAssembler {
Expand All @@ -141,6 +155,8 @@ public class LoanScheduleAssembler {
private final CalendarInstanceRepository calendarInstanceRepository;
private final LoanUtilService loanUtilService;
private final LoanDisbursementDetailsAssembler loanDisbursementDetailsAssembler;
private final LoanRepositoryWrapper loanRepositoryWrapper;
private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine;

public LoanApplicationTerms assembleLoanTerms(final JsonElement element) {
final Long loanProductId = this.fromApiJsonHelper.extractLongNamed("productId", element);
Expand Down Expand Up @@ -296,13 +312,6 @@ private LoanApplicationTerms assembleLoanApplicationTermsFrom(final JsonElement
if ((loanType.isJLGAccount() || loanType.isGroupAccount()) && calendar != null) {
validateRepaymentsStartDateWithMeetingDates(calculatedRepaymentsStartingFromDate, calendar, isSkipMeetingOnFirstDay,
numberOfDays);

/*
* If disbursement is synced on meeting, make sure disbursement date is on a meeting date
*/
if (synchDisbursement != null && synchDisbursement.booleanValue()) {
validateDisbursementDateWithMeetingDates(expectedDisbursementDate, calendar, isSkipMeetingOnFirstDay, numberOfDays);
}
}

if (RepaymentStartDateType.DISBURSEMENT_DATE.equals(repaymentStartDateType)) {
Expand Down Expand Up @@ -601,15 +610,6 @@ private void validateRepaymentsStartDateWithMeetingDates(final LocalDate repayme
}
}

public void validateDisbursementDateWithMeetingDates(final LocalDate expectedDisbursementDate, final Calendar calendar,
Boolean isSkipRepaymentOnFirstMonth, Integer numberOfDays) {
// disbursement date should fall on a meeting date
if (calendar != null && !calendar.isValidRecurringDate(expectedDisbursementDate, isSkipRepaymentOnFirstMonth, numberOfDays)) {
final String errorMessage = "Expected disbursement date '" + expectedDisbursementDate + "' do not fall on a meeting date";
throw new LoanApplicationDateException("disbursement.date.do.not.match.meeting.date", errorMessage, expectedDisbursementDate);
}
}

private void validateRepaymentFrequencyIsSameAsMeetingFrequency(final Integer meetingFrequency, final Integer repaymentFrequency,
final Integer meetingInterval, final Integer repaymentInterval) {
// meeting with daily frequency should allow loan products with any frequency.
Expand Down Expand Up @@ -1382,4 +1382,76 @@ public void updateLoanApplicationAttributes(JsonCommand command, Loan loan, Map<
loanProductRelatedDetail.setEqualAmortization(newValue);
}
}

public Pair<Loan, Map<String, Object>> assembleLoanApproval(AppUser currentUser, JsonCommand command, Long loanId) {
final JsonArray disbursementDataArray = command.arrayOfParameterNamed(LoanApiConstants.disbursementDataParameterName);
final Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true);

final LoanStatus newStatus = defaultLoanLifecycleStateMachine.dryTransition(LoanEvent.LOAN_APPROVED, loan);
if (newStatus.hasStateOf(loan.getStatus())) {
return Pair.of(loan, Map.of()); // no status change happened
}

final Map<String, Object> 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);
}
}
Loading

0 comments on commit 9b988e0

Please sign in to comment.