[ 
https://issues.apache.org/jira/browse/FINERACT-2492?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Ralph Hopman resolved FINERACT-2492.
------------------------------------
    Resolution: Fixed

> AprCalculator produces incorrect annual interest rate when DaysInYearType is 
> ACTUAL with WHOLE_TERM frequency
> -------------------------------------------------------------------------------------------------------------
>
>                 Key: FINERACT-2492
>                 URL: https://issues.apache.org/jira/browse/FINERACT-2492
>             Project: Apache Fineract
>          Issue Type: Bug
>          Components: Loan
>            Reporter: Ralph Hopman
>            Assignee: Ralph Hopman
>            Priority: Major
>
> When calculating annual nominal interest rates for loans using 
> {{interestRateFrequencyType = WHOLE_TERM}} combined with {{{}daysInYearType = 
> ACTUAL{}}}, the {{AprCalculator}} produces dramatically incorrect results. 
> The calculator multiplies the per-period rate by 
> {{daysInYearType.getValue()}} which returns {{1}} for ACTUAL instead of 
> {{{}365{}}}, resulting in interest charges that are ~365x lower than expected.
> h2. Steps to Reproduce
>  # Create a loan product with:
>  ** {{daysInYearType = ACTUAL}}
>  ** {{interestType = FLAT}}
>  ** {{interestRateFrequencyType = WHOLE_TERM}}
>  # Create a loan with:
>  ** {{principal = 3,400}}
>  ** {{interestRatePerPeriod = 10}} (expecting 10% total interest = 340)
>  ** {{numberOfRepayments = 3}}
>  ** {{repaymentEvery = 1}}
>  ** {{repaymentFrequencyType = DAYS}}
>  ** {{loanTermFrequency = 3}}
>  ** {{loanTermFrequencyType = DAYS}}
>  # Calculate loan schedule via {{POST /loans?command=calculateLoanSchedule}}
> h2. Expected Behavior
> *Annual rate calculation:*
> {code:java}
> ratePerPeriod = 10% ÷ 3 repayments = 3.333%
> annualRate = 3.333% × 365 days = 1,216.67%
> {code}
> *Interest for 3-day term:*
> {code:java}
> interestRate = (1,216.67% / 365 / 100) × 3 days = 10%
> totalInterest = 3,400 × 10% = 340.00
> {code}
> *Result:* Each of 3 installments should have *113.33* interest charge.
> h2. Actual Behavior
> *Annual rate calculation:*
> {code:java}
> ratePerPeriod = 10% ÷ 3 repayments = 3.333%
> annualRate = 3.333% × 1 = 3.333%  ← WRONG!
> {code}
> *Interest for 3-day term:*
> {code:java}
> interestRate = (3.333% / 365 / 100) × 3 days = 0.0274%
> totalInterest = 3,400 × 0.0274% = 0.93
> {code}
> *Result:* Total interest charged is only *0.93* instead of *340.00* (365x too 
> low).
> h2. Root Cause
> In {{{}AprCalculator.java{}}}, both the {{DAYS}} case (line 40) and the 
> {{WHOLE_TERM → DAYS}} case (line 56) use {{daysInYearType.getValue()}} 
> directly:
> {code:java}
> case DAYS:
>     defaultAnnualNominalInterestRate = interestRatePerPeriod
>         .multiply(BigDecimal.valueOf(daysInYearType.getValue()));  // 
> getValue() returns 1 for ACTUAL!
> break;
> {code}
> The {{DaysInYearType}} enum defines:
> {code:java}
> ACTUAL(1, "DaysInYearType.actual"),
> DAYS_360(360, "DaysInYearType.days360"),
> DAYS_364(364, "DaysInYearType.days364"),
> DAYS_365(365, "DaysInYearType.days365");
> {code}
> The value {{1}} for ACTUAL is a sentinel value intended to signal "use actual 
> days calculation" (handled properly in other parts of the codebase like 
> {{{}LoanApplicationTerms.calculatePeriodsInOneYear(){}}}). However, in 
> {{{}AprCalculator{}}}, this sentinel value is incorrectly used directly as a 
> multiplier for annualization.
> The correct pattern (used elsewhere in Fineract) is to delegate to 
> {{PaymentPeriodsInOneYearCalculator}} when {{{}daysInYearType == ACTUAL{}}}, 
> rather than using the enum's {{getValue()}} directly.
> h2. Solution
> Inject {{PaymentPeriodsInOneYearCalculator}} into {{AprCalculator}} and add a 
> helper method {{getDaysInYear()}} that delegates to the calculator when 
> {{{}daysInYearType == ACTUAL{}}}:
> {code:java}
> @Component
> @RequiredArgsConstructor
> public class AprCalculator {
>     private final PaymentPeriodsInOneYearCalculator 
> paymentPeriodsInOneYearCalculator;
>     public BigDecimal calculateFrom(..., final DaysInYearType daysInYearType) 
> {
>         // ... existing code ...
>         case DAYS:
>             defaultAnnualNominalInterestRate = interestRatePerPeriod
>                 .multiply(BigDecimal.valueOf(getDaysInYear(daysInYearType)));
>         break;
>         // ... similar for WHOLE_TERM → DAYS case ...
>     }
>     /**
>      * Helper method to get the number of days in a year, handling ACTUAL 
> appropriately.
>      * 
>      * When daysInYearType is ACTUAL, this delegates to the 
> PaymentPeriodsInOneYearCalculator
>      * (consistent with how Fineract handles ACTUAL elsewhere). For other 
> types (DAYS_360,
>      * DAYS_364, DAYS_365), it returns the configured value.
>      */
>     private int getDaysInYear(final DaysInYearType daysInYearType) {
>         // When ACTUAL, delegate to calculator (consistent with 
> LoanApplicationTerms.calculatePeriodsInOneYear)
>         if (daysInYearType == DaysInYearType.ACTUAL) {
>             return 
> paymentPeriodsInOneYearCalculator.calculate(PeriodFrequencyType.DAYS);
>         }
>         // For DAYS_360, DAYS_364, DAYS_365: use configured value
>         return daysInYearType.getValue();
>     }
> }
> {code}
> This approach:
>  * Delegates to 
> {{PaymentPeriodsInOneYearCalculator.calculate(PeriodFrequencyType.DAYS)}} 
> which returns {{365}}
>  * Follows the established pattern used in 
> {{LoanApplicationTerms.calculatePeriodsInOneYear()}}
>  * Maintains architectural consistency across the codebase
>  * Automatically benefits from any future enhancements to the calculator 
> (e.g., leap year support)
> h2. Testing Recommendations
>  # Add unit tests for {{AprCalculator.calculateFrom()}} covering:
>  ** {{DAYS}} frequency with {{DaysInYearType.ACTUAL}}
>  ** {{WHOLE_TERM}} frequency with {{DAYS}} repayment and 
> {{DaysInYearType.ACTUAL}}
>  ** Verify {{{}DAYS_360{}}}, {{{}DAYS_364{}}}, {{DAYS_365}} continue to work 
> correctly
>  ** Mock {{PaymentPeriodsInOneYearCalculator}} to verify delegation behavior
>  # Add integration test for loan schedule calculation with the reproduction 
> steps above
>  # Verify existing loans with {{DaysInYearType.ACTUAL}} produce correct 
> interest after fix



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to