adamsaghy commented on code in PR #6352:
URL: https://github.com/apache/fineract/pull/6352#discussion_r3981987959


##########
fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java:
##########
@@ -1743,6 +1691,7 @@ private void 
applyPrincipalMoratoriumIfRequired(List<RepaymentPeriod> repaymentP
         if (repaymentPeriods.isEmpty()) {
             return;
         }
+        repaymentPeriods.forEach(rp -> rp.setPrincipalPaymentGrace(false));

Review Comment:
   <head></head><p dir="ltr" style="caret-color: rgb(0, 0, 0); color: rgb(0, 0, 
0); font-style: normal; font-variant-caps: normal; font-weight: 400; 
letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; 
text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; 
-webkit-text-stroke-width: 0px; text-decoration-line: none; 
text-decoration-thickness: auto; text-decoration-style: solid;"><em>(anchor 
on<span class="Apple-converted-space"> 
</span><code>period.setPrincipalPaymentGrace(true);</code><span 
class="Apple-converted-space"> </span>inside<span 
class="Apple-converted-space"> 
</span><code>applyPrincipalMoratoriumIfRequired</code>)</em></p><p dir="ltr" 
style="caret-color: rgb(0, 0, 0); color: rgb(0, 0, 0); font-style: normal; 
font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 
2; text-align: start; text-indent: 0px; text-transform: none; white-space: 
normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-
 decoration-line: none; text-decoration-thickness: auto; text-decoration-style: 
solid;"><strong>Confirmed regression: the suffix-scoped grace marking collapses 
the tail of the schedule into an unintended balloon.</strong></p><p dir="ltr" 
style="caret-color: rgb(0, 0, 0); color: rgb(0, 0, 0); font-style: normal; 
font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 
2; text-align: start; text-indent: 0px; text-transform: none; white-space: 
normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; 
text-decoration-line: none; text-decoration-thickness: auto; 
text-decoration-style: solid;">The reset was widened to the full model, but 
the<span class="Apple-converted-space"> </span><em>marking</em><span 
class="Apple-converted-space"> </span>still counts against<span 
class="Apple-converted-space"> </span><code>relatedRepaymentPeriods</code><span 
class="Apple-converted-space"> </span>— the suffix from<span 
class="Apple-converted-space"> </span><code>
 calculateFromRepaymentPeriodDueDate</code>, not the loan:</p><div 
style="caret-color: rgb(0, 0, 0); color: rgb(0, 0, 0); font-style: normal; 
font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 
2; text-align: start; text-indent: 0px; text-transform: none; white-space: 
normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; 
text-decoration-line: none; text-decoration-thickness: auto; 
text-decoration-style: solid;"><div><div><div><pre>int gracePeriods = 
Math.min(graceOnPrincipalPayment, 
repaymentPeriods.size());</pre></div><div><div></div></div></div></div></div><p 
dir="ltr" style="caret-color: rgb(0, 0, 0); color: rgb(0, 0, 0); font-style: 
normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; 
orphans: 2; text-align: start; text-indent: 0px; text-transform: none; 
white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 
0px; text-decoration-line: none; text-decoration-thickness: auto; 
text-decoration-sty
 le: solid;">When the suffix is no longer than<span 
class="Apple-converted-space"> </span><code>graceOnPrincipalPayment</code>, 
every remaining period is flagged and set interest-only, and the method returns 
at<span class="Apple-converted-space"> </span><code>if (gracePeriods == 
repaymentPeriods.size())</code><span class="Apple-converted-space"> 
</span>before re-amortizing. The new skip in<span 
class="Apple-converted-space"> </span><code>getEmiAdjustment</code><span 
class="Apple-converted-space"> </span>then makes every pair skippable, so it 
returns the zero-difference fallback,<span class="Apple-converted-space"> 
</span><code>shouldBeAdjusted()</code><span class="Apple-converted-space"> 
</span>is false, and<span class="Apple-converted-space"> 
</span><code>checkAndAdjustEmiIfNeededOnRelatedRepaymentPeriods</code><span 
class="Apple-converted-space"> </span>breaks on its first iteration. On develop 
that step repaired exactly this state.</p><h4 dir="ltr" style="caret-color: rgb
 (0, 0, 0); color: rgb(0, 0, 0); font-style: normal; font-variant-caps: normal; 
letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; 
text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; 
-webkit-text-stroke-width: 0px; text-decoration-line: none; 
text-decoration-thickness: auto; text-decoration-style: solid;">Example</h4><p 
dir="ltr" style="caret-color: rgb(0, 0, 0); color: rgb(0, 0, 0); font-style: 
normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; 
orphans: 2; text-align: start; text-indent: 0px; text-transform: none; 
white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 
0px; text-decoration-line: none; text-decoration-thickness: auto; 
text-decoration-style: solid;">Progressive DECLINING_BALANCE, principal 100, 
30% p.a., 360/30, N=8 monthly,<span class="Apple-converted-space"> 
</span><code>graceOnPrincipalPayment=3</code><span 
class="Apple-converted-space"> </span>— a valid configuration, si
 nce<span class="Apple-converted-space"> 
</span><code>validateRepaymentPeriodWithGraceSettings</code><span 
class="Apple-converted-space"> </span>only rejects<span 
class="Apple-converted-space"> </span><code>grace &gt;= N</code>.</p><ol 
dir="ltr" start="1" style="caret-color: rgb(0, 0, 0); color: rgb(0, 0, 0); 
font-style: normal; font-variant-caps: normal; font-weight: 400; 
letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; 
text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; 
-webkit-text-stroke-width: 0px; text-decoration-line: none; 
text-decoration-thickness: auto; text-decoration-style: 
solid;"><li><code>addDisbursement(model, 1 January 2024, 
100)</code></li><li><code>changeInterestRate(model, 1 July 2024, 
24)</code><span class="Apple-converted-space"> </span>— effective date is<span 
class="Apple-converted-space"> 
</span><code>submittedOnDate.minusDays(1)</code><span 
class="Apple-converted-space"> </span>= 30 June 2024, so the suffi
 x is p6/p7/p8 and<span class="Apple-converted-space"> </span><code>min(3, 3) 
== size</code>.</li></ol><div dir="ltr" style="caret-color: rgb(0, 0, 0); 
color: rgb(0, 0, 0); font-style: normal; font-variant-caps: normal; 
font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; 
text-indent: 0px; text-transform: none; white-space: normal; widows: 2; 
word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-line: none; 
text-decoration-thickness: auto; text-decoration-style: solid;"><div>
   period | develop | this PR
   -- | -- | --
   p6 | emi 21.42, principal 19.89 | emi 1.53, principal 0.00
   p7 | emi 21.42, principal 20.59 | emi 0.83, principal 0.00
   p8 | emi 21.42, principal 21.00 | emi 61.90, principal 60.27
   
   </div></div><p dir="ltr" style="caret-color: rgb(0, 0, 0); color: rgb(0, 0, 
0); font-style: normal; font-variant-caps: normal; font-weight: 400; 
letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; 
text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; 
-webkit-text-stroke-width: 0px; text-decoration-line: none; 
text-decoration-thickness: auto; text-decoration-style: solid;">Total principal 
still reconciles to 100.00, but the customer's last two installments drop to 
interest-only and a 61.90 balloon appears out of nowhere. Reachable in 
production from<span class="Apple-converted-space"> 
</span><code>ProgressiveLoanScheduleGenerator:273</code><span 
class="Apple-converted-space"> </span>(interest-rate term variation during 
schedule regeneration) and<span class="Apple-converted-space"> 
</span><code>AdvancedPaymentScheduleTransactionProcessor:531</code>.</p><p 
dir="ltr" style="caret-color: rgb(0, 0, 0); color: rgb(0, 0, 0); font-style: 
normal; f
 ont-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 
2; text-align: start; text-indent: 0px; text-transform: none; white-space: 
normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; 
text-decoration-line: none; text-decoration-thickness: auto; 
text-decoration-style: solid;">This is the case my earlier second-tranche probe 
did not reach — there I concluded "not a regression, but the new flag makes it 
sticky". That still holds for a suffix longer than the grace count; it does not 
hold here.</p><h4 dir="ltr" style="caret-color: rgb(0, 0, 0); color: rgb(0, 0, 
0); font-style: normal; font-variant-caps: normal; letter-spacing: normal; 
orphans: 2; text-align: start; text-indent: 0px; text-transform: none; 
white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 
0px; text-decoration-line: none; text-decoration-thickness: auto; 
text-decoration-style: solid;">Probable fix</h4><p dir="ltr" 
style="caret-color: rgb(0, 0, 0); color: rg
 b(0, 0, 0); font-style: normal; font-variant-caps: normal; font-weight: 400; 
letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; 
text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; 
-webkit-text-stroke-width: 0px; text-decoration-line: none; 
text-decoration-thickness: auto; text-decoration-style: solid;">Anchor the 
marking to the loan, the same way the reset now is, and derive the amortizing 
set from the flag instead of from a<span class="Apple-converted-space"> 
</span><code>subList</code><span class="Apple-converted-space"> 
</span>offset:</p><div style="caret-color: rgb(0, 0, 0); color: rgb(0, 0, 0); 
font-style: normal; font-variant-caps: normal; font-weight: 400; 
letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; 
text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; 
-webkit-text-stroke-width: 0px; text-decoration-line: none; 
text-decoration-thickness: auto; text-decoration-style: solid;"><div><div>
 <div><pre>private void 
applyPrincipalMoratoriumIfRequired(List&lt;RepaymentPeriod&gt; repaymentPeriods,
           ProgressiveLoanInterestScheduleModel scheduleModel) {
       if (repaymentPeriods.isEmpty()) {
           return;
       }
       final List&lt;RepaymentPeriod&gt; allPeriods = 
scheduleModel.repaymentPeriods();
       allPeriods.forEach(rp -&gt; rp.setPrincipalPaymentGrace(false));
       final Integer graceOnPrincipalPayment = 
scheduleModel.loanProductRelatedDetail().getGraceOnPrincipalPayment();
       if (graceOnPrincipalPayment == null || graceOnPrincipalPayment &lt;= 0) {
           return;
       }
       final int gracePeriods = Math.min(graceOnPrincipalPayment, 
allPeriods.size());
       final LocalDate sliceFirstDueDate = 
repaymentPeriods.getFirst().getDueDate();
       allPeriods.subList(0, gracePeriods).forEach(period -&gt; {
           period.setPrincipalPaymentGrace(true);
           // only rewrite EMI for periods inside the slice being recalculated
           if (!period.getDueDate().isBefore(sliceFirstDueDate)) {
               final Money interestOnlyEmi = period.getDueInterest();
               period.setEmi(interestOnlyEmi);
               period.setOriginalEmi(interestOnlyEmi);
           }
       });
       final List&lt;RepaymentPeriod&gt; amortizingPeriods = 
repaymentPeriods.stream()
               .filter(rp -&gt; !rp.isPrincipalPaymentGrace()).toList();
       if (amortizingPeriods.isEmpty()) {
           return;
       }
       calculateOutstandingBalance(scheduleModel);
       calculateEMIOnActualModel(amortizingPeriods, scheduleModel);
   }</pre></div><div><div></div></div></div></div></div><p dir="ltr" 
style="caret-color: rgb(0, 0, 0); color: rgb(0, 0, 0); font-style: normal; 
font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 
2; text-align: start; text-indent: 0px; text-transform: none; white-space: 
normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; 
text-decoration-line: none; text-decoration-thickness: auto; 
text-decoration-style: solid;">Two things this buys us:</p><ul dir="ltr" 
style="caret-color: rgb(0, 0, 0); color: rgb(0, 0, 0); font-style: normal; 
font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 
2; text-align: start; text-indent: 0px; text-transform: none; white-space: 
normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; 
text-decoration-line: none; text-decoration-thickness: auto; 
text-decoration-style: solid;"><li>Grace lands on the loan's actual grace 
window on every recalculation, which also clears up the pre-exis
 ting mis-marking from the second-tranche probe.</li><li>The "whole suffix is 
grace" state becomes structurally unreachable. The suffix always contains the 
last repayment period, and<span class="Apple-converted-space"> 
</span><code>grace &gt;= N</code><span class="Apple-converted-space"> </span>is 
rejected at both product and loan level, so<span class="Apple-converted-space"> 
</span><code>amortizingPeriods</code><span class="Apple-converted-space"> 
</span>is never empty in practice.</li></ul><p dir="ltr" style="caret-color: 
rgb(0, 0, 0); color: rgb(0, 0, 0); font-style: normal; font-variant-caps: 
normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: 
start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; 
word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-line: none; 
text-decoration-thickness: auto; text-decoration-style: solid;">Note the date 
comparison rather than<span class="Apple-converted-space"> </span><code>repayme
 ntPeriods.contains(period)</code>:<span class="Apple-converted-space"> 
</span><code>RepaymentPeriod</code><span class="Apple-converted-space"> 
</span>carries<span class="Apple-converted-space"> 
</span><code>@EqualsAndHashCode</code>, so<span class="Apple-converted-space"> 
</span><code>contains</code><span class="Apple-converted-space"> </span>is a 
deep value comparison, not identity — it could match the wrong period and is 
O(n²) over the interest-period graph. Filtering on<span 
class="Apple-converted-space"> </span><code>dueDate</code><span 
class="Apple-converted-space"> </span>mirrors how<span 
class="Apple-converted-space"> 
</span><code>getRelatedRepaymentPeriods</code><span 
class="Apple-converted-space"> </span>builds the slice in the first 
place.</p><h4 dir="ltr" style="caret-color: rgb(0, 0, 0); color: rgb(0, 0, 0); 
font-style: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 
2; text-align: start; text-indent: 0px; text-transform: none; white-space
 : normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; 
text-decoration-line: none; text-decoration-thickness: auto; 
text-decoration-style: solid;">Test worth adding</h4><p dir="ltr" 
style="caret-color: rgb(0, 0, 0); color: rgb(0, 0, 0); font-style: normal; 
font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 
2; text-align: start; text-indent: 0px; text-transform: none; white-space: 
normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; 
text-decoration-line: none; text-decoration-thickness: auto; 
text-decoration-style: solid;">A unit test for a recalculation whose related 
suffix is entirely inside the grace count — the N-2 and N-1 tests both start 
from a full-schedule recalculation, so neither reaches this path. Asserting 
equal installments across p6/p7/p8 after the rate change above would pin it.</p>



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to