Cocoa-Puffs commented on PR #6369:
URL: https://github.com/apache/fineract/pull/6369#issuecomment-5569355098

   I've addressed all 3 concerns:
   
   **1. Claim: persisted rate changes silently dropped, then persisted away**
   
   This was unfortunately true. Currently there is no similar mechanism for WC 
loans that exists for term loans in regards to regenerating loans with outdated 
model versions. I have implemented the same functionality that we have for 
progressive loans.
   
   |# | Piece | Location|
   |-- | -- | --|
   1 | Version queries | ProjectedAmortizationLoanModelRepository -> 
existsByLoanIdAndJsonModelVersionNot, findLoanIdsRequiringModelRecalculation
   2 | Processing service | WorkingCapitalLoanModelProcessingService (new) -> 
requiresModelRecalculation, findLoanIdsRequiringModelRecalculation, 
recalculateModelAndSave in REQUIRES_NEW
   3 | Rebuild entry point | rebuildScheduleModelFromRecordedHistory on the 
write service
   4 | COB hook | AbstractWorkingCapitalLoanCOBWorkerItemProcessor.process, 
rebuilding before business steps; constructors threaded through both concrete 
processors and both configs
   5 | API filter | WorkingCapitalLoanModelCheckerFilter + 
WorkingCapitalLoanModelCheckerHelper (new), wired in SecurityConfig
   
   **2. Claim: closed schedules grow a zero row per elapsed day and move the 
maturity date**
   
   This was a real bug that is now fixed.
   
   Where it failed: _ProjectedAmortizationScheduleModel.minimumScheduleDays()_
   
   ```java
   int minimum = elapsedPeriodCount() + 1;   // the calendar, not recorded facts
   ``` 
   
   That floor only ever bit on a loan whose balance closes before today. While 
a loan still owes something the walk reaches today anyway, it bills until the 
balance closes and an unpaid day doesn't bring that closer.
   
   The fix:
   ```java
   int minimum = 1;
   final int offset = currentFirstPeriodDayOffset();
   for (final ActualPayment payment : actualPayments) {
       minimum = Math.max(minimum, resolvePaymentIndex(payment.date(), offset) 
+ 1);
   }
   for (final PrincipalAdjustment adjustment : principalAdjustments) {
       minimum = Math.max(minimum, resolvePaymentIndex(adjustment.date(), 
offset) + 1);
   }
   ``` 
   
   Not "exit when closed": a payment dated after the closing day currently 
reaches the schedule only because the elapsed floor drags the walk out to it. A 
naive closed-check would silently drop it. scheduleTerm() still carries 
elapsedPeriodCount() + 1 and is untouched, it governs the valid date range for 
a payment or rate change, which should extend to today.
   
   New unit test validating the behaviour: 
aClosedScheduleGrowsToReachMoneyButNotMerelyToReachToday
   
   **3. Claim: residual gap in the forward projection**
   
   This is now fixed. Three details, each load-bearing:
   
   1. Split the rates. dailyPayment = TPV × rate / npvDayCount / 100 has no 
balance term, so re-solving at the same period rate returns the same instalment 
— only eir and term move. The projection can therefore have its own rate while 
PlanCursor keeps the contractual one. Recognised income is untouched by 
construction.
   
   2. Pair exact with exact. Using discountFee − aggregatedHighPrecisionActual 
makes the sum exactly net + fee − collected, so solving from a position the 
plan already predicted returns the rate it already had: the re-solve is a no-op 
for an on-time payer.
   
   3. Re-solve lazily. Days with a record are always a run from the start, so 
one solve from the last of them re-prices the whole tail:
   
   ```java
   if (projectionStale && !settled) {
       final BigDecimal unearnedFee = 
discountFee.subtract(aggregatedHighPrecisionActual, mc);
       if (balance.signum() > 0 && unearnedFee.signum() > 0) {
           try {
               projection = AmortizationParams.solve(balance, unearnedFee, 
totalPaymentVolume, rateInForce, ...);
           } catch (final IllegalArgumentException | IllegalStateException | 
ArithmeticException e) {
               log.debug("Could not re-price the projection from balance {} 
with {} of fee unearned", balance, unearnedFee, e);
           }
       }
       projectionStale = false;
   }
   ``` 
   
   5,705 → 470 solves, output bit-for-bit identical to solving on every settled 
day, and the cost problem of resolving for every day disappears.


-- 
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