Skip to content

Commit

Permalink
FINERACT-2090: restructure loan approvals
Browse files Browse the repository at this point in the history
  • Loading branch information
kjozsa committed Jun 20, 2024
1 parent 0292171 commit da60607
Show file tree
Hide file tree
Showing 23 changed files with 364 additions and 357 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public interface ConfigurationDomainService {

boolean isPrincipalCompoundingDisabledForOverdueLoans();

Long retreivePeroidInNumberOfDaysForSkipMeetingDate();
Long retreivePeriodInNumberOfDaysForSkipMeetingDate();

boolean isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1789,114 +1788,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 @@ -1912,24 +1808,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 @@ -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<String> disbursementParameters = new HashSet<>(Arrays.asList(LoanApiConstants.rejectedOnDateParameterName,
LoanApiConstants.noteParameterName, LoanApiConstants.localeParameterName, LoanApiConstants.dateFormatParameterName));

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 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ private Collection<LocalDate> 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<LocalDate> recurringDates = CalendarUtils.getRecurringDates(rrule, seedDate, periodStartDate, periodEndDate,
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,7 @@ public Collection<StaffCenterData> 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,
Expand Down
Loading

0 comments on commit da60607

Please sign in to comment.