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

Reply via email to