FINERACT-2455: Store and return annual effective interest rate on WC loans - #6343
Conversation
8a41859 to
c731a83
Compare
galovics
left a comment
There was a problem hiding this comment.
Replacing the hardcoded pow(365) with npvDayCount is a genuine correctness fix, and I like the legacy-model fallback in annualEffectiveInterestRate() for schedules persisted before this change. But there's a scope-collision problem that needs sorting out before anything else here.
This directly overlaps with #6264 ("payment rate change EIR history"), on the same JIRA ticket. Both PRs touch 9 of the same files, including ProjectedAmortizationScheduleModel.java and the WC account Avro schema. Both replace the exact same hardcoded pow(365) call in WorkingCapitalLoanApplicationReadPlatformServiceImpl, but with two different helper methods living in two different classes. And the two PRs make mutually exclusive contract changes: #6264 keeps dailyEir on the response/event and documents it in a new Avro record; this PR deletes dailyEir from the response, both mappers, and the existing Avro schema. Whichever of these two merges second is going to break the other. This needs to be resolved between the two PRs - either merge them, or have one explicitly build on top of the other, before either goes further.
Two issues on the substance, both of which also apply to #6264 (already flagged there) but are worse here since this PR feeds the rounded value back into the schedule math:
Tenant money rounding mode applied to a rate, and that rate then drives the schedule. normalizedAnnualRate rounds with mc.getRoundingMode(), which is the tenant-configurable money rounding mode - and the result is fed straight back through deannualize to become effectiveInterestRate, the value the whole amortization schedule discounts on. A tenant configured FLOOR and one configured CEILING will get materially different WC schedules for identical loan inputs. A rate isn't money; this should use a fixed rounding mode (#6264 has already been corrected to RoundingMode.HALF_EVEN for the same issue - worth reusing that constant if the two PRs get reconciled).
The rate-segment's annual EIR is computed and then thrown away. computeScheduleParams returns annualEir in ScheduleParams, but applyRateChange only pulls segment.eir() and drops segment.annualEir() on the floor - so after a payment rate change, the reported annual rate goes stale while the schedule itself is segment-aware. That directly contradicts the new Swagger text ("annual effective rate the schedule runs on").
One more concrete bug: deannualize's Newton-Raphson seed (Math.pow(...)) can produce Infinity for an extreme daily IRR (reachable on a short schedule with a large discount fee relative to net disbursement), which then throws NumberFormatException out of BigDecimal.valueOf. The one existing caller of this path swallows it by accident (NumberFormatException extends IllegalArgumentException), but applyRateChange's new call site has no such guard and would surface this as a 500. Also: zero unit tests for the new TvmFunctions numerical kernel (hand-rolled Newton-Raphson, no test class at all).
Recommendation: CHANGES_REQUESTED
c731a83 to
0a0f63e
Compare
0a0f63e to
f29e7bb
Compare
|
993422d to
5c62f49
Compare
5c62f49 to
895cf54
Compare
|
895cf54 to
fc555df
Compare
Fixed |
|
@oleksii-novikov-onix Please rebase |
fc555df to
5584313
Compare
Done |
galovics
left a comment
There was a problem hiding this comment.
Four of the five things from last round are genuinely fixed, and well - the rounding mode is now a documented fixed constant (RoundingMode.HALF_EVEN) rather than the tenant's money mode, the Newton-Raphson overflow is fixed by splitting the decimal exponent instead of going through double, and TvmFunctions now has 16 unit tests covering round-trips and both overflow directions. The rate-segment staleness is now a documented design decision rather than an accident, though it does depend on #6264 for a way to see the current in-force rate.
The collision with #6264 is still completely open though, and it's sharper than before: both PRs were pushed within the same minute today, both still touch the same 9 files, and now they define the same field incompatibly - this PR makes calculatedAnnualEir a rounded percentage while #6264 keeps it an unrounded fraction, on the same endpoint. #6264's new Swagger text ("Note: periodPaymentRateHistory[].calculatedAnnualEir is a percentage" contrasted with the fraction at the top level) would be actively wrong the moment this PR lands. This needs to be resolved between the two PRs - ideally by having this PR read the annual rate off #6264's Solved record instead of maintaining a second annualization helper.
Beyond the collision, CI is actually red right now, and it looks like the same root cause across all of the failures: the rate round-trip (solve IRR -> round to 6dp annual -> spread back to a periodic rate) moves real cents in the amortization, and the expected values in the test suite were only partially updated to match - ProjectedAmortizationScheduleCalculatorTest and one e2e scenario both show the sweep stopped partway through. And the dailyEir removal is actually failing the project's own run-api-backward-compatibility gate right now (4 confirmed R014 errors), not just something that will show up as a warning.
One more: this modifies the WorkingCapitalLoanAccountDataV1 avro schema in place (deleting dailyEir, redefining calculatedAnnualEir's meaning) rather than creating a V2, which the project's own reliable-event-framework doc says should happen for exactly this kind of change to a published V1 record.
There's also an unrelated commit in here (SavingsInterestPostingJobIntegrationTest business-date pinning) that has nothing to do with WC or annual EIR - should be its own PR.
To get this to green: resolve the collision with #6264 (one unit, one helper), fix the CI failures (either the amortization drift is intentional and every expected value needs updating, or the round-trip shouldn't be perturbing the schedule at all), restore dailyEir or get sign-off on removing it, and split out the unrelated commit.
Recommendation: CHANGES_REQUESTED
7e3db67 to
034da5a
Compare
Collision with #6264. Nine files overlap, and that line in On reading the value off #6264's Solved, it works the other way round. Solved already exists in develop, and this PR is the one adding calculatedAnnualEir to it. What we suggest instead: CI. Fixed, and green on the current head. dailyEir and the R014 gate. Confirmed. The four errors are one field on four endpoints: the template, the paged list, {loanId} and external-id/{loanExternalId}. Removing it is what the AC asks for: "let's return annual effective interest rate (only) on loan details". Avro V1 versus V2. The rule is in the docs, but it has never been used for WC: there is no *V2.avsc file anywhere in the WC module. This schema was created by this ticket in July and has already been amended in place once since, and both changes were merged. I'm fine with creating a V2 if you want the rule applied to WC. @adamsaghy What do you think? The savings commit. Agreed, and done. It is out of this branch (three commits now, all WC) and raised separately as #6422. |
|
@oleksii-novikov-onix Please rebase We dont need v2, Working capital is not released officially. |
…nnual effective interest rate on WC loans
5334b85 to
3256680
Compare
3256680 to
1027c8a
Compare
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.