Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,24 @@ public void createWcDelinquencyBucket(final int frequency, final String frequenc
frequencyType, minimumPayment, minimumPaymentType);
}

@When("Admin creates WC delinquency reset action")
public void createResetAction() {
final PostWorkingCapitalLoansDelinquencyActionRequest request = new PostWorkingCapitalLoansDelinquencyActionRequest();
request.setAction("reset");
request.setLocale("en");

executeRescheduleAction(request);
}

@When("Admin creates WC delinquency undo reset action")
public void createUndoResetAction() {
final PostWorkingCapitalLoansDelinquencyActionRequest request = new PostWorkingCapitalLoansDelinquencyActionRequest();
request.setAction("undo_reset");
request.setLocale("en");

executeRescheduleAction(request);
}

@When("Admin creates WC delinquency reschedule action with the following parameters:")
public void createRescheduleAction(final DataTable table) {
final Map<String, String> params = table.asMaps().get(0);
Expand Down Expand Up @@ -208,6 +226,8 @@ public void verifyPeriods(final DataTable table) {
final List<WorkingCapitalLoanDelinquencyRangeScheduleData> periods = ok(
() -> fineractFeignClient.workingCapitalLoanDelinquencyRangeSchedule().retrieveDelinquencyRangeSchedule(loanId));

log.info("Periods: {}", periods);

final List<Map<String, String>> expectedRows = table.asMaps();
assertThat(periods).as("Number of periods").hasSize(expectedRows.size());

Expand Down Expand Up @@ -318,9 +338,15 @@ private void verifyFullScheduleField(final WorkingCapitalLoanDelinquencyRangeSch
case "periodNumber" -> assertThat(actual.getPeriodNumber()).as(label).isEqualTo(Integer.parseInt(expected));
case "fromDate" -> assertThat(actual.getFromDate()).as(label).isEqualTo(LocalDate.parse(expected, DATE_FORMAT));
case "toDate" -> assertThat(actual.getToDate()).as(label).isEqualTo(LocalDate.parse(expected, DATE_FORMAT));
case "expectedAmount" -> assertThat(actual.getExpectedAmount()).as(label).isEqualByComparingTo(new BigDecimal(expected));
case "paidAmount" -> assertThat(actual.getPaidAmount()).as(label).isEqualByComparingTo(new BigDecimal(expected));
case "outstandingAmount" -> assertThat(actual.getOutstandingAmount()).as(label).isEqualByComparingTo(new BigDecimal(expected));
case "expectedAmount" -> verifyOptionalField(expected,
v -> assertThat(actual.getExpectedAmount()).as(label).isEqualByComparingTo(new BigDecimal(expected)),
() -> assertThat(actual.getExpectedAmount()).as(label).isNull());
case "paidAmount" -> verifyOptionalField(expected,
v -> assertThat(actual.getPaidAmount()).as(label).isEqualByComparingTo(new BigDecimal(expected)),
() -> assertThat(actual.getPaidAmount()).as(label).isNull());
case "outstandingAmount" -> verifyOptionalField(expected,
v -> assertThat(actual.getOutstandingAmount()).as(label).isEqualByComparingTo(new BigDecimal(expected)),
() -> assertThat(actual.getOutstandingAmount()).as(label).isNull());
case "minPaymentCriteriaMet" -> verifyOptionalField(expected,
v -> assertThat(actual.getMinPaymentCriteriaMet()).as(label).isEqualTo(Boolean.parseBoolean(v)),
() -> assertThat(actual.getMinPaymentCriteriaMet()).as(label).isNull());
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,7 @@
public enum DelinquencyAction {
PAUSE, //
RESUME, //
RESCHEDULE //
RESCHEDULE, //
RESET, //
UNDO_RESET //
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,20 @@ public class WorkingCapitalLoanDelinquencyRangeSchedule extends AbstractAuditabl
@Column(name = "delinquent_amount", scale = 6, precision = 19)
private BigDecimal delinquentAmount;

@Column(name = "reset")
private Boolean reset = false;

public void reset() {
setReset(true);
setDelinquentDays(null);
setDelinquentAmount(null);
setMinPaymentCriteriaMet(null);
setPaidAmount(null);
setOutstandingAmount(null);
}

public BigDecimal getExpectedAmount() {
return reset ? null : this.expectedAmount;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ public interface WorkingCapitalLoanDelinquencyRangeScheduleTagHistoryMapper {
@AfterMapping
default void calculateTotal(WorkingCapitalLoanDelinquencyRangeScheduleTagHistory source,
@MappingTarget WorkingCapitalLoanDelinquencyTagHistoryData target) {
target.setDelinquentDays(source.getRangeSchedule().getDelinquentDays() - source.getDelinquencyRange().getMinimumAgeDays() + 1);
if (source.getRangeSchedule().getDelinquentDays() != null) {
target.setDelinquentDays(source.getRangeSchedule().getDelinquentDays() - source.getDelinquencyRange().getMinimumAgeDays() + 1);
}
}

@Mapping(target = "rangeId", source = "delinquencyRange.id")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public interface WorkingCapitalLoanDelinquencyRangeScheduleRepository
WHERE s.loan.id = :loanId
AND s.toDate < :transactionDate
AND (s.minPaymentCriteriaMet IS NULL OR s.minPaymentCriteriaMet = FALSE)
AND s.reset = false
ORDER BY s.periodNumber ASC""")
List<WorkingCapitalLoanDelinquencyRangeSchedule> findPastOpenPeriodsForRepayment(@Param("loanId") Long loanId,
@Param("transactionDate") LocalDate transactionDate);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyAction;
import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan;
import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanDelinquencyAction;
Expand All @@ -40,11 +41,15 @@
@RequiredArgsConstructor
public class WorkingCapitalLoanDelinquencyActionWriteServiceImpl implements WorkingCapitalLoanDelinquencyActionWriteService {

private static final String RESET_RESOURCE_NAME_FOR_PERMISSIONS = "WC_DELINQUENCY_RESET";
private static final String UNDO_RESET_RESOURCE_NAME_FOR_PERMISSIONS = "WC_DELINQUENCY_UNDO_RESET";

private final WorkingCapitalLoanRepository loanRepository;
private final WorkingCapitalLoanDelinquencyActionRepository actionRepository;
private final WorkingCapitalLoanDelinquencyActionParseAndValidator validator;
private final WorkingCapitalLoanDelinquencyRangeScheduleService rangeScheduleService;
private final WorkingCapitalLoanDelinquencyRangeScheduleService delinquencyRangeScheduleService;
private final PlatformSecurityContext context;

@Transactional
@Override
Expand All @@ -69,6 +74,15 @@ public CommandProcessingResult createDelinquencyAction(final Long workingCapital
final WorkingCapitalLoanDelinquencyAction activePause = validator.findActivePauseForResume(existing,
DateUtils.getBusinessLocalDate());
rangeScheduleService.resumeActivePause(workingCapitalLoan, activePause, action);
} else if (DelinquencyAction.RESET.equals(action.getAction())) {
context.authenticatedUser().validateHasCreatePermission(RESET_RESOURCE_NAME_FOR_PERMISSIONS);
rangeScheduleService.resetPeriods(workingCapitalLoan, action);
} else if (DelinquencyAction.UNDO_RESET.equals(action.getAction())) {
context.authenticatedUser().validateHasCreatePermission(UNDO_RESET_RESOURCE_NAME_FOR_PERMISSIONS);
List<WorkingCapitalLoanDelinquencyAction> byWorkingCapitalLoanIdOrderById = actionRepository
.findByWorkingCapitalLoanIdOrderById(workingCapitalLoanId);
rangeScheduleService.undoResetPeriods(workingCapitalLoan, action, byWorkingCapitalLoanIdOrderById);
rangeScheduleService.reprocessDelinquencySchedule(workingCapitalLoan);
}

return new CommandProcessingResultBuilder() //
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,16 @@
package org.apache.fineract.portfolio.workingcapitalloan.service;

import java.time.LocalDate;
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange;
import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan;
import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanDelinquencyRangeSchedule;

public interface WorkingCapitalLoanDelinquencyClassificationService {

void instantClassifyDelinquency(WorkingCapitalLoan loan, LocalDate businessDate);

void classifyDelinquency(WorkingCapitalLoan loan, LocalDate businessDate);

void applyDelinquencyTagForRange(WorkingCapitalLoan loan, WorkingCapitalLoanDelinquencyRangeSchedule range,
DelinquencyRange currentRange, LocalDate businessDate);
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public void classifyDelinquency(WorkingCapitalLoan loan, LocalDate businessDate)
.findByLoanIdOrderByPeriodNumberAsc(loan.getId());

for (WorkingCapitalLoanDelinquencyRangeSchedule range : delinquencyRangeScheduleList) {
if (range.getToDate().isBefore(businessDate)) {
if (!Objects.equals(range.getReset(), true) && range.getToDate().isBefore(businessDate)) {
long rangeDelinquentDays = range.getOutstandingAmount().compareTo(BigDecimal.ZERO) > 0
? DateUtils.getDifferenceInDays(range.getToDate(), businessDate)
: 0L;
Expand Down Expand Up @@ -137,6 +137,7 @@ public Optional<DelinquencyRange> findDelinquencyRangeByDays(final DelinquencyBu
* @param businessDate
* the date on which the tagging operation is performed
*/
@Override
public void applyDelinquencyTagForRange(final WorkingCapitalLoan loan, final WorkingCapitalLoanDelinquencyRangeSchedule range,
final DelinquencyRange currentRange, final LocalDate businessDate) {
List<WorkingCapitalLoanDelinquencyRangeScheduleTagHistory> updatedList = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,8 @@ void resumeActivePause(WorkingCapitalLoan loan, WorkingCapitalLoanDelinquencyAct
*/
void reprocessDelinquencySchedule(WorkingCapitalLoan loan);

void resetPeriods(WorkingCapitalLoan workingCapitalLoan, WorkingCapitalLoanDelinquencyAction action);

void undoResetPeriods(WorkingCapitalLoan workingCapitalLoan, WorkingCapitalLoanDelinquencyAction action,
List<WorkingCapitalLoanDelinquencyAction> byWorkingCapitalLoanIdOrderById);
}
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ private List<WorkingCapitalLoanDelinquencyRangeSchedule> resetAllPeriodsForRepro
final List<WorkingCapitalLoanDelinquencyRangeSchedule> periods = loanDelinquencyRangeScheduleRepository
.findByLoanIdOrderByPeriodNumberAsc(loanId);
for (final WorkingCapitalLoanDelinquencyRangeSchedule period : periods) {
period.setPaidAmount(BigDecimal.ZERO);
period.setPaidAmount(period.getReset() ? null : BigDecimal.ZERO);
period.setOutstandingAmount(period.getExpectedAmount());
period.setMinPaymentCriteriaMet(null);
period.setDelinquentAmount(null);
Expand All @@ -195,8 +195,8 @@ private void allocateRepayment(final WorkingCapitalLoan loan, final LocalDate tr
for (WorkingCapitalLoanDelinquencyRangeSchedule period : pastOpenPeriods) {
BigDecimal payAmount = MathUtil.min(transactionAmount, period.getOutstandingAmount(), true);
transactionAmount = transactionAmount.subtract(payAmount);
period.setPaidAmount(period.getPaidAmount().add(payAmount));
period.setOutstandingAmount(period.getOutstandingAmount().subtract(payAmount));
period.setPaidAmount(MathUtil.nullToZero(period.getPaidAmount()).add(payAmount));
period.setOutstandingAmount(MathUtil.nullToZero(period.getOutstandingAmount()).subtract(payAmount));
if (period.getOutstandingAmount().compareTo(BigDecimal.ZERO) <= 0) {
period.setMinPaymentCriteriaMet(true);
period.setDelinquentAmount(BigDecimal.ZERO);
Expand All @@ -213,8 +213,8 @@ private void allocateRepayment(final WorkingCapitalLoan loan, final LocalDate tr
WorkingCapitalLoanDelinquencyRangeSchedule period = currentPeriod.get();
BigDecimal newPaidAmount = period.getPaidAmount().add(transactionAmount);
period.setPaidAmount(newPaidAmount);
period.setOutstandingAmount(period.getExpectedAmount().subtract(newPaidAmount).max(BigDecimal.ZERO));
if (newPaidAmount.compareTo(period.getExpectedAmount()) >= 0) {
period.setOutstandingAmount(MathUtil.nullToZero(period.getExpectedAmount()).subtract(newPaidAmount).max(BigDecimal.ZERO));
if (newPaidAmount.compareTo(MathUtil.nullToZero(period.getExpectedAmount())) >= 0) {
period.setMinPaymentCriteriaMet(true);
period.setDelinquentAmount(BigDecimal.ZERO);
period.setDelinquentDays(0L);
Expand All @@ -230,6 +230,9 @@ public void evaluateExpiredPeriods(WorkingCapitalLoan loan, LocalDate businessDa
List<WorkingCapitalLoanDelinquencyRangeSchedule> unevaluatedPeriods = loanDelinquencyRangeScheduleRepository
.findByLoanIdAndToDateLessThanEqualAndMinPaymentCriteriaMetIsNull(loan.getId(), businessDate);
for (WorkingCapitalLoanDelinquencyRangeSchedule period : unevaluatedPeriods) {
if (period.getReset()) {
continue;
}
boolean criteriaMet = period.getPaidAmount().compareTo(period.getExpectedAmount()) >= 0;
period.setMinPaymentCriteriaMet(criteriaMet);
loanDelinquencyRangeScheduleRepository.saveAndFlush(period);
Expand Down Expand Up @@ -307,6 +310,39 @@ public void resumeActivePause(final WorkingCapitalLoan loan, final WorkingCapita
recalculateDelinquencyAfterPauseResume(loan, businessDate);
}

@Override
public void resetPeriods(WorkingCapitalLoan loan, WorkingCapitalLoanDelinquencyAction action) {
LocalDate resetDate = action.getStartDate();
final List<WorkingCapitalLoanDelinquencyRangeSchedule> periods = loanDelinquencyRangeScheduleRepository
.findByLoanIdOrderByPeriodNumberAsc(loan.getId());
periods.stream().filter(p -> !Objects.equals(p.getReset(), true) && p.getToDate().isBefore(action.getStartDate()))
.forEach(p1 -> resetPeriod(p1, resetDate));
}

private void resetPeriod(WorkingCapitalLoanDelinquencyRangeSchedule period, LocalDate resetDate) {
period.reset();
delinquencyClassificationService.applyDelinquencyTagForRange(period.getLoan(), period, null, resetDate);
}

@Override
public void undoResetPeriods(WorkingCapitalLoan loan, WorkingCapitalLoanDelinquencyAction action,
List<WorkingCapitalLoanDelinquencyAction> byWorkingCapitalLoanIdOrderById) {

List<WorkingCapitalLoanDelinquencyAction> activeResets = byWorkingCapitalLoanIdOrderById.stream()
.filter(a -> DelinquencyAction.RESET.equals(a.getAction()) && a.getEndDate() == null).toList();
if (!activeResets.isEmpty()) {
activeResets.getLast().setEndDate(action.getStartDate());
}
LocalDate lastActiveResetStartDate = activeResets.size() >= 2 ? activeResets.get(activeResets.size() - 2).getStartDate() : null;
final List<WorkingCapitalLoanDelinquencyRangeSchedule> periods = loanDelinquencyRangeScheduleRepository
.findByLoanIdOrderByPeriodNumberAsc(loan.getId());
periods.stream()
.filter(p -> Objects.equals(p.getReset(), true) && p.getToDate().isBefore(action.getStartDate())
&& (lastActiveResetStartDate == null || !p.getToDate().isBefore(lastActiveResetStartDate)))
.forEach(p1 -> p1.setReset(false));
reprocessDelinquencySchedule(loan);
}

private void shrinkPeriodsForPause(final WorkingCapitalLoan loan, final LocalDate pauseStart, final LocalDate originalPauseEnd,
final LocalDate newPauseEnd) {
final long daysToRemove = WorkingCapitalLoanDelinquencyPauseUtils.calculateDaysRemovedOnResume(pauseStart, newPauseEnd,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ public WorkingCapitalLoanDelinquencyAction validateAndParse(final JsonCommand co
validateReschedule(parsedAction, workingCapitalLoan, dataValidator);
} else if (DelinquencyAction.RESUME.equals(parsedAction.getAction())) {
validateResume(parsedAction, existing, dataValidator);
} else if (DelinquencyAction.RESET.equals(parsedAction.getAction())) {
validateReset(parsedAction, dataValidator);
} else if (DelinquencyAction.UNDO_RESET.equals(parsedAction.getAction())) {
validateUndoReset(parsedAction, dataValidator);
}

throwExceptionIfValidationWarningsExist(dataValidator);
Expand Down Expand Up @@ -120,6 +124,22 @@ private void validateResume(final WorkingCapitalLoanDelinquencyAction action, fi
}
}

private void validateReset(WorkingCapitalLoanDelinquencyAction action, DataValidatorBuilder dataValidator) {
final LocalDate businessDate = DateUtils.getBusinessLocalDate();
dataValidator.reset().parameter(START_DATE).value(action.getStartDate()).ignoreIfNull().isOneOfTheseValues(businessDate);
if (action.getEndDate() == null) {
action.setStartDate(businessDate);
}
}

private void validateUndoReset(WorkingCapitalLoanDelinquencyAction action, DataValidatorBuilder dataValidator) {
final LocalDate businessDate = DateUtils.getBusinessLocalDate();
dataValidator.reset().parameter(START_DATE).value(action.getStartDate()).ignoreIfNull().isOneOfTheseValues(businessDate);
if (action.getEndDate() == null) {
action.setStartDate(businessDate);
}
}

private void validateResumeStartDate(final WorkingCapitalLoanDelinquencyAction action, final LocalDate businessDate,
final DataValidatorBuilder dataValidator) {
dataValidator.reset().parameter(START_DATE).value(action.getStartDate()).notNull();
Expand Down Expand Up @@ -179,6 +199,10 @@ private WorkingCapitalLoanDelinquencyAction parseCommand(final JsonCommand comma
action.setFrequencyType(extractFrequencyType(json, dataValidator));
} else if (DelinquencyAction.RESUME.equals(action.getAction())) {
action.setStartDate(extractDate(json, START_DATE));
} else if (DelinquencyAction.RESET.equals(action.getAction())) {
action.setStartDate(extractDate(json, START_DATE));
} else if (DelinquencyAction.UNDO_RESET.equals(action.getAction())) {
action.setStartDate(extractDate(json, START_DATE));
}

return action;
Expand All @@ -196,6 +220,10 @@ private DelinquencyAction extractAction(final JsonElement json, final DataValida
return DelinquencyAction.RESCHEDULE;
} else if ("resume".equalsIgnoreCase(actionString)) {
return DelinquencyAction.RESUME;
} else if ("reset".equalsIgnoreCase(actionString)) {
return DelinquencyAction.RESET;
} else if ("undo_reset".equalsIgnoreCase(actionString)) {
return DelinquencyAction.UNDO_RESET;
}
failParameterValidation(dataValidator, ACTION, "invalid.action",
"Invalid Delinquency Action: " + actionString + ". Supported actions: pause, reschedule, resume");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,5 @@
<include relativeToChangelogFile="true" file="parts/0048_rename_wc_accounting_type_cash_based_to_acc_def_rev_am.xml"/>
<include relativeToChangelogFile="true" file="parts/0049_wc_loan_breach_reset.xml"/>
<include relativeToChangelogFile="true" file="parts/0050_wc_loan_breach_disable.xml"/>
<include relativeToChangelogFile="true" file="parts/0051_delinquency_reset.xml"/>
</databaseChangeLog>
Loading
Loading