Skip to content

Commit 71c7b2b

Browse files
committed
FINERACT-2455: Simplify reset logic
1 parent 347694a commit 71c7b2b

14 files changed

Lines changed: 243 additions & 269 deletions

fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalBreachDisable.feature

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,7 @@ Feature: Working Capital Breach Disable
500500
And Admin creates WC breach reset action
501501
Then Working Capital loan breach schedule has the following data:
502502
| periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | nearBreach | breach |
503-
| 1 | 2026-01-01 | 2026-02-28 | 110.70 | 0 | null | null |
503+
| 1 | 2026-01-01 | 2026-02-28 | 110.70 | 0 | null | false |
504504
When Admin sets the business date to "20 January 2026"
505505
And Admin runs inline COB job for Working Capital Loan by loanId
506506
And Admin initiate a Working Capital loan breach disable with startDate "20 January 2026"
@@ -509,7 +509,7 @@ Feature: Working Capital Breach Disable
509509
And Admin initiate a Working Capital loan breach enable with startDate "01 March 2026"
510510
Then Working Capital loan breach schedule has the following data:
511511
| periodNumber | fromDate | toDate | minPaymentAmount | outstandingAmount | nearBreach | breach |
512-
| 1 | 2026-01-01 | 2026-02-28 | 110.70 | 110.70 | null | true |
512+
| 1 | 2026-01-01 | 2026-02-28 | 110.70 | 0 | null | false |
513513
| 2 | 2026-03-01 | 2026-04-30 | 110.70 | 110.70 | null | null |
514514
When Admin closes the Working Capital loan with a full repayment on "01 March 2026"
515515
Then Working Capital loan status will be "CLOSED_OBLIGATIONS_MET"

fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalBreachReset.feature

Lines changed: 40 additions & 40 deletions
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.fineract.portfolio.workingcapitalloan.data;
20+
21+
import java.math.BigDecimal;
22+
import java.time.LocalDate;
23+
24+
public record TransactionDateAndAmountHolder(LocalDate transactionDate, BigDecimal transactionAmount) {
25+
}

fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanBreachResetHistory.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ public class WorkingCapitalLoanBreachResetHistory extends AbstractAuditableWithU
4545
@JoinColumn(name = "breach_schedule_id", nullable = false)
4646
private WorkingCapitalLoanBreachSchedule breachSchedule;
4747

48+
@Column(name = "min_payment_amount", scale = 6, precision = 19)
49+
private BigDecimal minPaymentAmount;
50+
4851
@Column(name = "outstanding_amount", scale = 6, precision = 19)
4952
private BigDecimal outstandingAmount;
5053

@@ -54,4 +57,12 @@ public class WorkingCapitalLoanBreachResetHistory extends AbstractAuditableWithU
5457
@Column(name = "near_breach")
5558
private Boolean nearBreach;
5659

60+
public WorkingCapitalLoanBreachResetHistory(WorkingCapitalLoanBreachAction resetAction, WorkingCapitalLoanBreachSchedule period) {
61+
setBreachAction(resetAction);
62+
setBreachSchedule(period);
63+
setOutstandingAmount(period.getOutstandingAmount());
64+
setBreach(period.getBreach());
65+
setNearBreach(period.getNearBreach());
66+
setMinPaymentAmount(period.getMinPaymentAmount());
67+
}
5768
}

fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanBreachSchedule.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,18 @@ public class WorkingCapitalLoanBreachSchedule extends AbstractAuditableWithUTCDa
7171
@Column(name = "breach")
7272
private Boolean breach;
7373

74+
@Column(name = "reset", nullable = false)
75+
private boolean reset;
76+
77+
public void reset() {
78+
this.reset = true;
79+
this.breach = null;
80+
this.nearBreach = null;
81+
this.outstandingAmount = null;
82+
this.paidAmount = null;
83+
}
84+
85+
public BigDecimal getMinPaymentAmount() {
86+
return reset ? null : this.minPaymentAmount;
87+
}
7488
}

fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanBreachActionRepository.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,12 @@ select case when count(action) > 0 then true else false end from WorkingCapitalL
4141
""")
4242
boolean isBreachDisabledAsOf(@Param("loanId") Long loanId, @Param("date") LocalDate date);
4343

44+
@Query("""
45+
SELECT ba FROM WorkingCapitalLoanBreachAction ba
46+
WHERE ba.workingCapitalLoan.id= :workingCapitalLoanId
47+
AND ba.action IN :actionTypes
48+
ORDER BY ba.startDate, ba.id
49+
""")
50+
List<WorkingCapitalLoanBreachAction> findByLoanAndActionType(@Param("workingCapitalLoanId") Long workingCapitalLoanId,
51+
@Param("actionTypes") List<WorkingCapitalLoanBreachActionType> actionTypes);
4452
}

fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanTransactionRepository.java

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@
2020

2121
import java.math.BigDecimal;
2222
import java.time.LocalDate;
23-
import java.util.Collection;
2423
import java.util.List;
2524
import java.util.Optional;
2625
import org.apache.fineract.infrastructure.core.domain.ExternalId;
2726
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
27+
import org.apache.fineract.portfolio.workingcapitalloan.data.TransactionDateAndAmountHolder;
2828
import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransaction;
2929
import org.springframework.data.domain.Page;
3030
import org.springframework.data.domain.Pageable;
@@ -79,14 +79,13 @@ BigDecimal sumDiscountFeeAdjustmentsAfter(@Param("wcLoanId") Long wcLoanId,
7979
boolean existsByExternalId(ExternalId externalId);
8080

8181
@Query("""
82-
select coalesce(sum(t.transactionAmount), 0)
83-
from WorkingCapitalLoanTransaction t
84-
where t.wcLoan.id = :wcLoanId and t.reversed = false
85-
and t.transactionType in :transactionTypes
86-
and t.transactionDate >= :fromDate and t.transactionDate <= :toDate
82+
SELECT t.transactionDate, SUM(t.transactionAmount)
83+
FROM WorkingCapitalLoanTransaction t
84+
WHERE t.reversed = FALSE
85+
AND t.wcLoan.id = :wcLoanId
86+
AND t.transactionType in :transactionTypes
87+
GROUP BY t.transactionDate
8788
""")
88-
BigDecimal sumBreachRelevantPaid(@Param("wcLoanId") Long wcLoanId,
89-
@Param("transactionTypes") Collection<LoanTransactionType> transactionTypes, @Param("fromDate") LocalDate fromDate,
90-
@Param("toDate") LocalDate toDate);
91-
89+
List<TransactionDateAndAmountHolder> fetchTransactionDateAndAmount(@Param("wcLoanId") Long wcLoanId,
90+
@Param("transactionTypes") List<LoanTransactionType> transactionTypes);
9291
}

fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanActiveBreachResetResolver.java

Lines changed: 14 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,10 @@
2121
import java.time.LocalDate;
2222
import java.util.ArrayDeque;
2323
import java.util.Deque;
24-
import java.util.HashSet;
2524
import java.util.List;
26-
import java.util.Objects;
2725
import java.util.Optional;
28-
import java.util.Set;
2926
import lombok.RequiredArgsConstructor;
27+
import org.apache.fineract.infrastructure.core.service.DateUtils;
3028
import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanBreachAction;
3129
import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanBreachActionType;
3230
import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanBreachActionRepository;
@@ -39,71 +37,25 @@ public class WorkingCapitalLoanActiveBreachResetResolver {
3937
private final WorkingCapitalLoanBreachActionRepository breachActionRepository;
4038

4139
public Optional<WorkingCapitalLoanBreachAction> findLatestActiveReset(final Long workingCapitalLoanId) {
42-
return findLatestActiveReset(workingCapitalLoanId, null);
43-
}
44-
45-
public Optional<LocalDate> findLatestActiveResetDate(final Long workingCapitalLoanId) {
46-
return findLatestActiveReset(workingCapitalLoanId).map(WorkingCapitalLoanBreachAction::getStartDate);
47-
}
48-
49-
public Optional<WorkingCapitalLoanBreachAction> findLatestActiveReset(final Long workingCapitalLoanId,
50-
final Long maxActionIdExclusive) {
51-
if (workingCapitalLoanId == null) {
52-
return Optional.empty();
53-
}
54-
final List<WorkingCapitalLoanBreachAction> actions = loadActions(workingCapitalLoanId, maxActionIdExclusive);
55-
final Optional<Long> latestActiveResetId = findLatestActiveResetId(actions);
56-
return latestActiveResetId
57-
.flatMap(activeResetId -> actions.stream().filter(action -> Objects.equals(action.getId(), activeResetId)).findFirst());
40+
List<WorkingCapitalLoanBreachAction> filtereredList = breachActionRepository.findByLoanAndActionType(workingCapitalLoanId,
41+
List.of(WorkingCapitalLoanBreachActionType.RESET, WorkingCapitalLoanBreachActionType.UNDO_RESET));
42+
Deque<WorkingCapitalLoanBreachAction> stack = new ArrayDeque<>();
43+
filtereredList.forEach(action -> {
44+
if (action.getAction().equals(WorkingCapitalLoanBreachActionType.RESET)) {
45+
stack.push(action);
46+
} else if (action.getAction().equals(WorkingCapitalLoanBreachActionType.UNDO_RESET)) {
47+
stack.pop();
48+
}
49+
});
50+
return stack.isEmpty() ? Optional.empty() : Optional.of(stack.peek());
5851
}
5952

6053
public boolean hasActiveReset(final Long workingCapitalLoanId) {
6154
return findLatestActiveReset(workingCapitalLoanId).isPresent();
6255
}
6356

6457
public boolean existsActiveResetInPeriod(final Long workingCapitalLoanId, final LocalDate fromDate, final LocalDate toDate) {
65-
if (workingCapitalLoanId == null || fromDate == null || toDate == null) {
66-
return false;
67-
}
68-
final List<WorkingCapitalLoanBreachAction> actions = loadActions(workingCapitalLoanId, null);
69-
final Set<Long> activeResetIds = findActiveResetIds(actions);
70-
return actions.stream().filter(action -> WorkingCapitalLoanBreachActionType.RESET.equals(action.getAction()))
71-
.filter(action -> action.getId() != null && activeResetIds.contains(action.getId()))
72-
.anyMatch(action -> isDateWithinPeriod(action.getStartDate(), fromDate, toDate));
73-
}
74-
75-
private List<WorkingCapitalLoanBreachAction> loadActions(final Long workingCapitalLoanId, final Long maxActionIdExclusive) {
76-
return breachActionRepository.findByWorkingCapitalLoanIdOrderById(workingCapitalLoanId).stream()
77-
.filter(action -> action.getId() != null && (maxActionIdExclusive == null || action.getId() < maxActionIdExclusive))
78-
.toList();
79-
}
80-
81-
private Optional<Long> findLatestActiveResetId(final List<WorkingCapitalLoanBreachAction> actions) {
82-
final Deque<Long> activeResetIds = buildActiveResetStack(actions);
83-
return activeResetIds.isEmpty() ? Optional.empty() : Optional.of(activeResetIds.peekLast());
58+
return findLatestActiveReset(workingCapitalLoanId).filter(a -> DateUtils.isDateInRangeInclusive(a.getStartDate(), fromDate, toDate))
59+
.isPresent();
8460
}
85-
86-
private Set<Long> findActiveResetIds(final List<WorkingCapitalLoanBreachAction> actions) {
87-
return new HashSet<>(buildActiveResetStack(actions));
88-
}
89-
90-
private Deque<Long> buildActiveResetStack(final List<WorkingCapitalLoanBreachAction> actions) {
91-
final Deque<Long> activeResetIds = new ArrayDeque<>();
92-
for (final WorkingCapitalLoanBreachAction action : actions) {
93-
if (action == null || action.getId() == null) {
94-
continue;
95-
}
96-
if (WorkingCapitalLoanBreachActionType.RESET.equals(action.getAction())) {
97-
activeResetIds.addLast(action.getId());
98-
} else if (WorkingCapitalLoanBreachActionType.UNDO_RESET.equals(action.getAction()) && !activeResetIds.isEmpty()) {
99-
activeResetIds.removeLast();
100-
}
101-
}
102-
return activeResetIds;
103-
}
104-
105-
private boolean isDateWithinPeriod(final LocalDate date, final LocalDate fromDate, final LocalDate toDate) {
106-
return date != null && !date.isBefore(fromDate) && !date.isAfter(toDate);
107-
}
108-
10961
}

fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachActionWriteServiceImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public CommandProcessingResult createBreachAction(final Long workingCapitalLoanI
7272
.reduce((first, second) -> second).ifPresent(action -> {
7373
action.setEndDate(breachAction.getStartDate().minusDays(1));
7474
});
75-
breachScheduleService.reEvaluateAfterEnable(workingCapitalLoan, breachAction.getStartDate());
75+
breachScheduleService.reprocessBreachSchedule(workingCapitalLoan);
7676
}
7777

7878
return new CommandProcessingResultBuilder() //

0 commit comments

Comments
 (0)