[
https://issues.apache.org/jira/browse/FINERACT-3201?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Senesh Perera updated FINERACT-3201:
------------------------------------
Description:
h3. Background
Split out of FINERACT-3194, as suggested in the review there. FINERACT-3049 (PR
https://github.com/apache/fineract/pull/6827) makes cumulative loans leap-year
aware for DAILY + ACTUAL. The progressive engine already is (FINERACT-2197).
Both engines start from the stored annual nominal rate. For a rate entered *per
day*, that annual rate is always built with 365 days, so in a leap year the
per-day rate the loan actually charges is lower than the one entered.
This ticket asks for a decision on what a per-day rate means under ACTUAL. It
is not a bug fix.
h3. Current behavior (verified on develop d63beed)
*1. Conversion to an annual rate*
{{AprCalculator.calculateFrom()}} converts the per-period rate to the annual
nominal rate:
* {{interestRateFrequencyType = DAYS}}: rate *
{{getDaysInYear(daysInYearType)}}.
* {{WHOLE_TERM}} with DAYS repayment frequency: same, after dividing by the
number of periods.
* For ACTUAL, {{getDaysInYear()}} returns
{{PaymentPeriodsInOneYearCalculator.calculate(DAYS)}}, a fixed 365. It has no
date input.
*2. Where it is called*
* Loan product create: {{LoanProductAssembler.java:138}}.
* Loan product update: {{LoanProductRelatedDetailUpdateUtil.java:410}}
({{updateInterestRateDerivedFields()}}).
* Loan application / schedule: {{LoanScheduleAssembler.java:285}}.
The product calls have no disbursement date, so a leap-year-aware annual rate
cannot be computed there. The stored annual nominal rate has to stay
date-independent.
*3. Per-day rates are accepted by the API*
* {{LoanProductDataValidator.java}} (lines 620, 1776) checks
{{interestRateFrequencyType}} with {{inMinMaxRange(0, 4)}}, so {{DAYS}} (0) is
accepted on product create and update.
* The product dropdown template
({{LoanDropdownReadPlatformServiceImpl.java:113-117}}) only offers {{MONTHS}},
{{YEARS}} and {{WHOLE_TERM}}.
* {{ProgressiveEMICalculator}} has no restriction on
{{interestRateFrequencyType}}; it reads only the stored annual nominal rate and
sets the calculation frequency from the repayment frequency (line 306). The
only check rejecting {{DAYS}} is for floating-rate products.
* At loan level, {{LoanApplicationValidator}} and {{LoanScheduleValidator}}
list {{interestRateFrequencyType}} as a supported parameter with no range
check; {{LoanScheduleAssembler}} reads it with
{{PeriodFrequencyType.fromInt()}}.
*4. Progressive products today*
{{ProgressiveEMICalculator}} uses the stored annual nominal rate and divides it
by {{DaysInYearType.getNumberOfDays(date)}}, which is {{lengthOfYear()}} for
ACTUAL (366 in a leap year).
So for a progressive product with 0.1% per day and ACTUAL:
* stored annual rate: 0.1 * 365 = 36.5%
* daily rate charged in 2024: 36.5 / 366 = 0.09972678%
* daily rate charged in 2025: 36.5 / 365 = 0.1%
*This already happens for progressive loans today and is not introduced by
FINERACT-3049.* After FINERACT-3049, cumulative loans behave the same way as
progressive loans.
h3. Examples
*1. Per-day rate (0.1% per day)*
10,000 principal, rate entered as 0.1% per day, ACTUAL, DAILY, one 31-day
period (with default {{isInterestRecognitionOnDisbursementDate = false}}).
Verified by running a unit test against the real {{AprCalculator}} and
{{ProgressiveEMICalculator}}:
|| Period || Days (365-day + 366-day) || Rate as entered (0.1% * days) ||
Progressive today, and cumulative after FINERACT-3049 ||
| 2024-01-01 to 2024-02-01 (leap year) | 0 + 31 | 310.00 | 309.15 |
| 2023-12-15 to 2024-01-15 (crosses year end) | 16 + 15 | 310.00 | 309.59 |
| 2025-01-01 to 2025-02-01 (control) | 31 + 0 | 310.00 | 310.00 |
_(With {{isInterestRecognitionOnDisbursementDate = true}}, the cross-year split
is 17 + 14 days and charges 309.62.)_
*2. Whole-term rate over days (the FINERACT-2492 scenario)*
FINERACT-2492 uses a 10% {{WHOLE_TERM}} rate on a 3-day FLAT loan of 3,400 with
DAYS repayment (340.00 expected at 10%). {{AprCalculator}} rounds 10/3 to 8
decimals ({{3.33333333}}) and multiplies by 365, storing an annual nominal rate
of {{1216.66666545%}}.
* Today the flat calculation divides by 365: each daily installment rounds to
113.33, giving *339.99* total in any year (1 cent below 340.00 due to the
8-decimal rounding of 10/3).
* Once flat interest is leap-year aware (FINERACT-3194), a 3-day loan with
DAILY interest calculation in 2024 divides by 366: the unrounded total is
339.07, and the schedule rounds each daily installment to 113.02, giving
*339.06* total (~9.97% instead of the 10% entered for the whole term).
* A whole-term rate states the total interest for the loan, so a leap year
should arguably not change it.
h3. Options
# *Keep and document.* Under ACTUAL, a per-day rate is converted to an annual
nominal rate with 365 days. In a leap year the daily rate charged is rate *
365/366. No code change; documentation and tests only.
# *Use the per-day rate directly.* When the rate is entered per day, the
engines charge rate * actual days, so the year length no longer matters. Both
cumulative and progressive engines need a branch for this. It changes the
interest of existing per-day ACTUAL loans in leap years.
# *Restrict the combination.* Reject {{interestRateFrequencyType = DAYS}} (and
{{WHOLE_TERM}} with DAYS repayment) together with ACTUAL. Simple, but it breaks
API users who use it today.
h3. Questions
# Which option is wanted?
# Is {{interestRateFrequencyType = DAYS}} intentionally accepted by the API,
given that the template does not offer it?
# For option 2: should the change apply to existing loans, or only to loans
created after the change?
# Should a {{WHOLE_TERM}} rate always produce the entered percentage over the
term, regardless of leap years?
h3. Out of scope
* {{WEEKS}} (* 52) and {{SEMI_MONTHLY}} (* 24) conversions: nominal and not
related to the year length.
* Flat interest: FINERACT-3194.
* The loan EMI: stays nominal, as agreed in FINERACT-3049.
h3. Related issues
* FINERACT-3194: flat interest, DAILY + ACTUAL (this ticket was split from it)
* FINERACT-3049: cumulative loans, DAILY + ACTUAL (PR #6827)
* FINERACT-2197: leap-year-aware days for progressive loans
* FINERACT-2492: AprCalculator fix for ACTUAL with WHOLE_TERM
* FINERACT-1982: daily charge accrual /365 vs /366 (open)
was:
h3. Background
Split out of FINERACT-3194 as suggested in review.
Both the progressive engine (FINERACT-2197) and the cumulative engine (after
FINERACT-3049 / PR #6827) calculate DAILY + ACTUAL interest by starting from
the stored *annual nominal interest rate* and dividing by the actual days in
the year (366 in a leap year, 365 otherwise).
However, when a user enters an interest rate *per day*
({{interestRateFrequencyType = DAYS}}, or {{WHOLE_TERM}} with {{DAYS}}
repayment frequency), {{AprCalculator.calculateFrom()}} always derives the
stored annual nominal rate using a fixed *365* days. Consequently, in a leap
year, the engine multiplies by 365 on input and divides by 366 during schedule
generation, charging slightly less than the entered per-day rate.
This ticket tracks the design decision on what a per-day interest rate should
mean when {{DaysInYearType}} is {{ACTUAL}}.
h3. Code Analysis (on develop d63beed)
*1. Annual nominal rate conversion ({{AprCalculator.java}})*
* {{AprCalculator.calculateFrom()}} converts a periodic rate to an annual
nominal rate:
** {{DAYS}}: {{rate * getDaysInYear(daysInYearType)}}
** {{WHOLE_TERM}} with {{DAYS}} repayment: {{(rate / numberOfPeriods) *
getDaysInYear(daysInYearType)}}
* For {{ACTUAL}}, {{getDaysInYear()}} delegates to
{{PaymentPeriodsInOneYearCalculator.calculate(DAYS)}}, which returns a
hardcoded {{365}} (it has no date context).
*2. Why {{AprCalculator}} cannot simply take a date*
* {{AprCalculator.calculateFrom()}} is called at loan application/schedule time
({{LoanScheduleAssembler.java:285}}), but also during loan product creation
({{LoanProductAssembler.java:138}}) and product updates
({{LoanProductRelatedDetailUpdateUtil.java:410}}).
* Because loan products have no disbursement date, the stored annual nominal
rate on the product must remain date-independent.
*3. API vs. UI support for per-day rates*
* *API:* {{LoanProductDataValidator.java}} (lines 620, 1776) validates
{{interestRateFrequencyType}} with {{inMinMaxRange(0, 4)}}, accepting {{0}}
({{DAYS}}). {{LoanApplicationValidator}} and {{LoanScheduleValidator}} also
accept it at the loan level.
* *UI Template:* {{LoanDropdownReadPlatformServiceImpl.java}} (lines 113–117)
only returns {{MONTHS}}, {{YEARS}}, and {{WHOLE_TERM}}.
* *Progressive Engine:* {{ProgressiveEMICalculator.java}} has no guard
rejecting {{DAYS}}; it consumes the stored annual nominal rate directly.
h3. Verified Behavior & Examples
*Case A: Progressive & Cumulative Loans (0.1% per day, 10,000 principal,
ACTUAL, DAILY, 31-day period)*
* {{AprCalculator}} stores the annual rate as {{0.1 * 365 = 36.5%}}.
* In 2024 (leap year), the daily rate applied is {{36.5% / 366 = 0.09972678%}}.
* In 2025 (non-leap year), the daily rate applied is {{36.5% / 365 = 0.1%}}.
* *Note:* This already occurs on {{develop}} today for progressive loans;
FINERACT-3049 makes cumulative loans match this behavior.
|| Period || Expected at 0.1% / day || Actual Schedule Output (Progressive
today & Cumulative after FINERACT-3049) ||
| 2024-01-01 to 2024-02-01 (leap year) | 310.00 | 309.15 |
| 2023-12-15 to 2024-01-15 (crosses year end)* | 310.00 | 309.59 |
| 2025-01-01 to 2025-02-01 (non-leap control) | 310.00 | 310.00 |
_(*Assumes default {{isInterestRecognitionOnDisbursementDate = false}}; with
{{true}}, the cross-year output is {{309.62}}.)_
*Case B: Whole-Term Rate over Days (FINERACT-2492 scenario)*
FINERACT-2492 tests a {{10%}} {{WHOLE_TERM}} rate on a 3-day {{FLAT}} loan of
{{3,400}} with {{DAYS}} repayment (expecting ~{{340.00}} total interest).
* {{AprCalculator}} rounds {{10 / 3}} to 8 decimals ({{3.33333333}}) and
multiplies by {{365}}, storing an annual nominal rate of {{1216.66666545%}}.
* In a 365-day year, the flat schedule rounds each daily installment to
{{113.33}}, totaling *{{339.99}}* (1 cent below {{340.00}} due to the 8-decimal
rounding).
* Once flat interest becomes leap-year aware (FINERACT-3194), a 3-day loan in
2024 divides by {{366}}, rounding each daily installment to {{113.02}} and
totaling *{{339.06}}* (~9.97% over the term instead of the entered 10%).
h3. Options for Decision
# *Keep and document (365-based nominal conversion):* Under {{ACTUAL}}, a
per-day rate is defined as a nominal rate annualized over 365 days, yielding
{{rate * 365 / 366}} per day in a leap year. Requires documentation and tests
only (no calculation change).
# *Apply the per-day rate directly in the engines:* When the rate is entered
with {{DAYS}} (or {{WHOLE_TERM}} over {{DAYS}}), bypass the 365/366 annual rate
round-trip and apply {{rate * actualDays}} directly in both cumulative and
progressive calculators. Changes interest on existing per-day {{ACTUAL}} loans
in leap years.
# *Restrict the combination:* Reject {{interestRateFrequencyType = DAYS}} (and
{{WHOLE_TERM}} with {{DAYS}} repayment) when {{daysInYearType = ACTUAL}} in the
validator. Simple, though potentially breaking for API callers relying on it.
h3. Questions for Maintainers
# Which of the three options is preferred?
# Is {{interestRateFrequencyType = DAYS}} intentionally supported by the API
despite being omitted from the UI dropdown template?
# Should a {{WHOLE_TERM}} rate always charge the exact entered percentage
across the loan term regardless of leap years?
# If Option 2 is chosen, should it apply only to newly created loans or also on
recalculation of existing loans?
h3. Out of Scope
* {{WEEKS}} (* 52) and {{SEMI_MONTHLY}} (* 24) conversions in {{AprCalculator}}
(purely nominal).
* Flat interest leap-year implementation (tracked in FINERACT-3194).
* Cumulative loan EMI (stays nominal 365-based by design, per FINERACT-3049).
h3. Related Issues
* FINERACT-3194: Flat interest: DAILY interest with ACTUAL days in year should
be leap-year aware
* FINERACT-3049: Cumulative loans: DAILY interest with ACTUAL days in year is
not leap-year aware (PR #6827)
* FINERACT-2197: Introduce a new type of ACTUAL during leap year for
progressive loans
* FINERACT-2492: AprCalculator produces incorrect annual nominal rate for
WHOLE_TERM with DAYS
> Define how a per-day interest rate is converted when days in year is ACTUAL
> (AprCalculator)
> -------------------------------------------------------------------------------------------
>
> Key: FINERACT-3201
> URL: https://issues.apache.org/jira/browse/FINERACT-3201
> Project: Apache Fineract
> Issue Type: Improvement
> Components: Loan
> Affects Versions: 1.16.1
> Reporter: Senesh Perera
> Assignee: Senesh Perera
> Priority: Minor
>
> h3. Background
> Split out of FINERACT-3194, as suggested in the review there. FINERACT-3049
> (PR https://github.com/apache/fineract/pull/6827) makes cumulative loans
> leap-year aware for DAILY + ACTUAL. The progressive engine already is
> (FINERACT-2197). Both engines start from the stored annual nominal rate. For
> a rate entered *per day*, that annual rate is always built with 365 days, so
> in a leap year the per-day rate the loan actually charges is lower than the
> one entered.
> This ticket asks for a decision on what a per-day rate means under ACTUAL. It
> is not a bug fix.
> h3. Current behavior (verified on develop d63beed)
> *1. Conversion to an annual rate*
> {{AprCalculator.calculateFrom()}} converts the per-period rate to the annual
> nominal rate:
> * {{interestRateFrequencyType = DAYS}}: rate *
> {{getDaysInYear(daysInYearType)}}.
> * {{WHOLE_TERM}} with DAYS repayment frequency: same, after dividing by the
> number of periods.
> * For ACTUAL, {{getDaysInYear()}} returns
> {{PaymentPeriodsInOneYearCalculator.calculate(DAYS)}}, a fixed 365. It has no
> date input.
> *2. Where it is called*
> * Loan product create: {{LoanProductAssembler.java:138}}.
> * Loan product update: {{LoanProductRelatedDetailUpdateUtil.java:410}}
> ({{updateInterestRateDerivedFields()}}).
> * Loan application / schedule: {{LoanScheduleAssembler.java:285}}.
> The product calls have no disbursement date, so a leap-year-aware annual rate
> cannot be computed there. The stored annual nominal rate has to stay
> date-independent.
> *3. Per-day rates are accepted by the API*
> * {{LoanProductDataValidator.java}} (lines 620, 1776) checks
> {{interestRateFrequencyType}} with {{inMinMaxRange(0, 4)}}, so {{DAYS}} (0)
> is accepted on product create and update.
> * The product dropdown template
> ({{LoanDropdownReadPlatformServiceImpl.java:113-117}}) only offers
> {{MONTHS}}, {{YEARS}} and {{WHOLE_TERM}}.
> * {{ProgressiveEMICalculator}} has no restriction on
> {{interestRateFrequencyType}}; it reads only the stored annual nominal rate
> and sets the calculation frequency from the repayment frequency (line 306).
> The only check rejecting {{DAYS}} is for floating-rate products.
> * At loan level, {{LoanApplicationValidator}} and {{LoanScheduleValidator}}
> list {{interestRateFrequencyType}} as a supported parameter with no range
> check; {{LoanScheduleAssembler}} reads it with
> {{PeriodFrequencyType.fromInt()}}.
> *4. Progressive products today*
> {{ProgressiveEMICalculator}} uses the stored annual nominal rate and divides
> it by {{DaysInYearType.getNumberOfDays(date)}}, which is {{lengthOfYear()}}
> for ACTUAL (366 in a leap year).
> So for a progressive product with 0.1% per day and ACTUAL:
> * stored annual rate: 0.1 * 365 = 36.5%
> * daily rate charged in 2024: 36.5 / 366 = 0.09972678%
> * daily rate charged in 2025: 36.5 / 365 = 0.1%
> *This already happens for progressive loans today and is not introduced by
> FINERACT-3049.* After FINERACT-3049, cumulative loans behave the same way as
> progressive loans.
> h3. Examples
> *1. Per-day rate (0.1% per day)*
> 10,000 principal, rate entered as 0.1% per day, ACTUAL, DAILY, one 31-day
> period (with default {{isInterestRecognitionOnDisbursementDate = false}}).
> Verified by running a unit test against the real {{AprCalculator}} and
> {{ProgressiveEMICalculator}}:
> || Period || Days (365-day + 366-day) || Rate as entered (0.1% * days) ||
> Progressive today, and cumulative after FINERACT-3049 ||
> | 2024-01-01 to 2024-02-01 (leap year) | 0 + 31 | 310.00 | 309.15 |
> | 2023-12-15 to 2024-01-15 (crosses year end) | 16 + 15 | 310.00 | 309.59 |
> | 2025-01-01 to 2025-02-01 (control) | 31 + 0 | 310.00 | 310.00 |
> _(With {{isInterestRecognitionOnDisbursementDate = true}}, the cross-year
> split is 17 + 14 days and charges 309.62.)_
> *2. Whole-term rate over days (the FINERACT-2492 scenario)*
> FINERACT-2492 uses a 10% {{WHOLE_TERM}} rate on a 3-day FLAT loan of 3,400
> with DAYS repayment (340.00 expected at 10%). {{AprCalculator}} rounds 10/3
> to 8 decimals ({{3.33333333}}) and multiplies by 365, storing an annual
> nominal rate of {{1216.66666545%}}.
> * Today the flat calculation divides by 365: each daily installment rounds to
> 113.33, giving *339.99* total in any year (1 cent below 340.00 due to the
> 8-decimal rounding of 10/3).
> * Once flat interest is leap-year aware (FINERACT-3194), a 3-day loan with
> DAILY interest calculation in 2024 divides by 366: the unrounded total is
> 339.07, and the schedule rounds each daily installment to 113.02, giving
> *339.06* total (~9.97% instead of the 10% entered for the whole term).
> * A whole-term rate states the total interest for the loan, so a leap year
> should arguably not change it.
> h3. Options
> # *Keep and document.* Under ACTUAL, a per-day rate is converted to an annual
> nominal rate with 365 days. In a leap year the daily rate charged is rate *
> 365/366. No code change; documentation and tests only.
> # *Use the per-day rate directly.* When the rate is entered per day, the
> engines charge rate * actual days, so the year length no longer matters. Both
> cumulative and progressive engines need a branch for this. It changes the
> interest of existing per-day ACTUAL loans in leap years.
> # *Restrict the combination.* Reject {{interestRateFrequencyType = DAYS}}
> (and {{WHOLE_TERM}} with DAYS repayment) together with ACTUAL. Simple, but it
> breaks API users who use it today.
> h3. Questions
> # Which option is wanted?
> # Is {{interestRateFrequencyType = DAYS}} intentionally accepted by the API,
> given that the template does not offer it?
> # For option 2: should the change apply to existing loans, or only to loans
> created after the change?
> # Should a {{WHOLE_TERM}} rate always produce the entered percentage over the
> term, regardless of leap years?
> h3. Out of scope
> * {{WEEKS}} (* 52) and {{SEMI_MONTHLY}} (* 24) conversions: nominal and not
> related to the year length.
> * Flat interest: FINERACT-3194.
> * The loan EMI: stays nominal, as agreed in FINERACT-3049.
> h3. Related issues
> * FINERACT-3194: flat interest, DAILY + ACTUAL (this ticket was split from it)
> * FINERACT-3049: cumulative loans, DAILY + ACTUAL (PR #6827)
> * FINERACT-2197: leap-year-aware days for progressive loans
> * FINERACT-2492: AprCalculator fix for ACTUAL with WHOLE_TERM
> * FINERACT-1982: daily charge accrual /365 vs /366 (open)
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)