Skip to content
Merged
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 a Working Capital delinquency reset")
public void createResetAction() {
final PostWorkingCapitalLoansDelinquencyActionRequest request = new PostWorkingCapitalLoansDelinquencyActionRequest();
request.setAction("reset");
request.setLocale("en");

executeRescheduleAction(request);
}

@When("Admin creates Working Capital delinquency reset undo")
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 @@ -275,7 +293,7 @@ public void verifySpecificPeriods(final DataTable table) {

private void executeRescheduleAction(final PostWorkingCapitalLoansDelinquencyActionRequest request) {
final Long loanId = getLoanId();
log.info("Creating RESCHEDULE action for WC loan {}: {}", loanId, request);
log.debug("Creating RESCHEDULE action for WC loan {}: {}", loanId, request);

final PostWorkingCapitalLoansDelinquencyActionResponse result = ok(
() -> fineractFeignClient.workingCapitalLoanDelinquencyActions().createDelinquencyAction(loanId, request));
Expand Down Expand Up @@ -318,9 +336,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 @@ -79,4 +79,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 @@ -24,6 +24,7 @@
import java.util.Optional;
import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanDelinquencyRangeSchedule;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

Expand All @@ -42,6 +43,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 All @@ -64,4 +66,28 @@ List<WorkingCapitalLoanDelinquencyRangeSchedule> findByLoanIdAndToDateLessThanEq

Optional<WorkingCapitalLoanDelinquencyRangeSchedule> findTopByLoanIdAndMinPaymentCriteriaMetFalseOrderByFromDateAsc(Long loanId);

List<WorkingCapitalLoanDelinquencyRangeSchedule> findByLoanIdAndResetIsNotAndToDateBeforeOrderByPeriodNumberAsc(Long id, Boolean reset,
LocalDate startDate);

@Modifying
@Query("""
update WorkingCapitalLoanDelinquencyRangeSchedule p
set p.reset = false
where p.loan.id = :loanId
and p.reset = true
and p.toDate < :actionStartDate
""")
int clearResetBeforeActionStartDate(@Param("loanId") Long loanId, @Param("actionStartDate") LocalDate actionStartDate);

@Modifying
@Query("""
update WorkingCapitalLoanDelinquencyRangeSchedule p
set p.reset = false
where p.loan.id = :loanId
and p.reset = true
and p.toDate < :actionStartDate
and p.toDate >= :lastActiveResetStartDate
""")
int clearResetBeforeActionStartDateFromLastActiveReset(@Param("loanId") Long loanId,
@Param("actionStartDate") LocalDate actionStartDate, @Param("lastActiveResetStartDate") LocalDate lastActiveResetStartDate);
}
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,10 +41,14 @@
@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 PlatformSecurityContext context;

@Transactional
@Override
Expand All @@ -68,6 +73,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 @@ -58,4 +58,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 @@ -168,7 +168,7 @@ private List<WorkingCapitalLoanDelinquencyRangeSchedule> resetAllPeriodsForRepro
if (period.getBaseExpectedAmount() != null) {
period.setExpectedAmount(period.getBaseExpectedAmount());
}
period.setPaidAmount(BigDecimal.ZERO);
period.setPaidAmount(period.getReset() ? null : BigDecimal.ZERO);
period.setOutstandingAmount(period.getExpectedAmount());
period.setMinPaymentCriteriaMet(null);
period.setDelinquentAmount(null);
Expand All @@ -187,8 +187,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 @@ -201,12 +201,12 @@ private void allocateRepayment(final WorkingCapitalLoan loan, final LocalDate tr
break;
}
}
if (currentPeriod.isPresent()) {
if (currentPeriod.isPresent() && !currentPeriod.get().getReset()) {
WorkingCapitalLoanDelinquencyRangeSchedule period = currentPeriod.get();
BigDecimal newPaidAmount = period.getPaidAmount().add(transactionAmount);
BigDecimal newPaidAmount = MathUtil.nullToZero(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 @@ -222,6 +222,9 @@ public void evaluateExpiredPeriods(WorkingCapitalLoan loan, LocalDate businessDa
List<WorkingCapitalLoanDelinquencyRangeSchedule> unevaluatedPeriods = loanDelinquencyRangeScheduleRepository
.findByLoanIdAndToDateLessThanEqualAndMinPaymentCriteriaMetIsNull(loan.getId(), businessDate);
for (WorkingCapitalLoanDelinquencyRangeSchedule period : unevaluatedPeriods) {
if (period.getReset()) {
continue;
}
capPeriodToRemainingBalance(period, loan);
boolean criteriaMet = period.getPaidAmount().compareTo(period.getExpectedAmount()) >= 0;
period.setMinPaymentCriteriaMet(criteriaMet);
Expand Down Expand Up @@ -294,6 +297,40 @@ 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
.findByLoanIdAndResetIsNotAndToDateBeforeOrderByPeriodNumberAsc(loan.getId(), true, action.getStartDate());
periods.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;

if (lastActiveResetStartDate == null) {
loanDelinquencyRangeScheduleRepository.clearResetBeforeActionStartDate(loan.getId(), action.getStartDate());
} else {
loanDelinquencyRangeScheduleRepository.clearResetBeforeActionStartDateFromLastActiveReset(loan.getId(), action.getStartDate(),
lastActiveResetStartDate);
}
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
Loading
Loading