FINERACT-2455: Rework WC loan schedule model to use recurrent calculations - #6369
Conversation
b3ead3d to
6668012
Compare
|
@Cocoa-Puffs Can you please review the below?
No 3 in details: The residual cent gap in the WC projectionThis note is about a smaller residue that survives it: after the The measurementWhat the two sides are"Owed" is exact by construction. "Billed" is governed by the balance recursion. Each tail row does So the tail bills what is owed only if its own accruals sum to exactly the fee still Why they don't match
It is visible on the closing row:
Why the ticket's own scenario shows 0.00Not because it has a single tail row. The fee is already fully earned at the last known day — So the ticket's third acceptance line is met for a reason that does not generalise — it needed the Why this is not a blocker
Worth raising anywayThe ticket's acceptance reads "the schedule accounts for the full 10000". On any schedule where the |
3305fcc to
130fa13
Compare
130fa13 to
a1e3c12
Compare
|
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.
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() int minimum = elapsedPeriodCount() + 1; // the calendar, not recorded factsThat 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: 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:
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. |
…tions - E2E tests
|
@Cocoa-Puffs I added a set of additional WC amortization e2e scenarios to stress the rework the other way around: mutate the loan, then force a near-payoff / backdated large repayment and assert exact totals (fee earned and payable to the cent, no silent drift). Coverage includes:
сс @ruzeynalov |
Description
Describe the changes made and why they were made. (Ignore if these details are present on the associated Apache Fineract JIRA ticket.)
Checklist
Please make sure these boxes are checked before submitting your pull request - thanks!
Your assigned reviewer(s) will follow our guidelines for code reviews.