@@ -3174,6 +3174,59 @@ public void test_leap_year_only_actual_for_loan_S5() {
31743174 checkPeriod (interestSchedule , 5 , 867.65 , 10.03 , 857.62 , 0.00 , false );
31753175 }
31763176
3177+ /**
3178+ * Cross-year edge case: quarterly repayment period spans from non-leap year (2023) into leap year (2024) and
3179+ * contains Feb 29. With FEB_29_PERIOD_ONLY strategy, this period should use 366 days (same as FULL_LEAP_YEAR)
3180+ * because Feb 29 falls within the period.
3181+ */
3182+ @ Test
3183+ public void test_feb29_period_only_cross_year_quarterly_period_containing_feb29 () {
3184+ final List <LoanScheduleModelRepaymentPeriod > expectedRepaymentPeriods = List .of (
3185+ periodData (LocalDate .of (2023 , 9 , 1 ), LocalDate .of (2023 , 12 , 1 )),
3186+ periodData (LocalDate .of (2023 , 12 , 1 ), LocalDate .of (2024 , 3 , 1 )),
3187+ periodData (LocalDate .of (2024 , 3 , 1 ), LocalDate .of (2024 , 6 , 1 )),
3188+ periodData (LocalDate .of (2024 , 6 , 1 ), LocalDate .of (2024 , 9 , 1 )));
3189+
3190+ final BigDecimal interestRate = BigDecimal .valueOf (12.0 );
3191+ final Integer installmentAmountInMultiplesOf = null ;
3192+
3193+ // Schedule with FEB_29_PERIOD_ONLY
3194+ Mockito .when (loanProductRelatedDetail .getAnnualNominalInterestRate ()).thenReturn (interestRate );
3195+ Mockito .when (loanProductRelatedDetail .getDaysInYearType ()).thenReturn (DaysInYearType .ACTUAL .getValue ());
3196+ Mockito .when (loanProductRelatedDetail .getDaysInYearCustomStrategy ())
3197+ .thenReturn (DaysInYearCustomStrategyType .FEB_29_PERIOD_ONLY );
3198+ Mockito .when (loanProductRelatedDetail .getDaysInMonthType ()).thenReturn (DaysInMonthType .ACTUAL .getValue ());
3199+ Mockito .when (loanProductRelatedDetail .getRepaymentPeriodFrequencyType ()).thenReturn (PeriodFrequencyType .MONTHS );
3200+ Mockito .when (loanProductRelatedDetail .getRepayEvery ()).thenReturn (3 );
3201+ Mockito .when (loanProductRelatedDetail .getCurrencyData ()).thenReturn (currency );
3202+ Mockito .when (loanProductRelatedDetail .isAllowFullTermForTranche ()).thenReturn (false );
3203+
3204+ final ProgressiveLoanInterestScheduleModel feb29Schedule = emiCalculator .generatePeriodInterestScheduleModel (
3205+ expectedRepaymentPeriods , loanProductRelatedDetail , installmentAmountInMultiplesOf , mc );
3206+ emiCalculator .addDisbursement (feb29Schedule , LocalDate .of (2023 , 9 , 1 ), toMoney (10000.0 ));
3207+
3208+ // Schedule with FULL_LEAP_YEAR
3209+ Mockito .when (loanProductRelatedDetail .getDaysInYearCustomStrategy ()).thenReturn (DaysInYearCustomStrategyType .FULL_LEAP_YEAR );
3210+
3211+ final ProgressiveLoanInterestScheduleModel fullLeapSchedule = emiCalculator .generatePeriodInterestScheduleModel (
3212+ expectedRepaymentPeriods , loanProductRelatedDetail , installmentAmountInMultiplesOf , mc );
3213+ emiCalculator .addDisbursement (fullLeapSchedule , LocalDate .of (2023 , 9 , 1 ), toMoney (10000.0 ));
3214+
3215+ // Period 1 (Dec 1, 2023 → Mar 1, 2024) contains Feb 29, 2024.
3216+ // Both strategies should use 366 days for this period, so interest should match.
3217+ final RepaymentPeriod feb29Period1 = feb29Schedule .repaymentPeriods ().get (1 );
3218+ final RepaymentPeriod fullLeapPeriod1 = fullLeapSchedule .repaymentPeriods ().get (1 );
3219+ Assertions .assertEquals (toDouble (fullLeapPeriod1 .getDueInterest ()), toDouble (feb29Period1 .getDueInterest ()),
3220+ "Cross-year period containing Feb 29 should use 366 days for both strategies" );
3221+
3222+ // Period 2 (Mar 1, 2024 → Jun 1, 2024) does NOT contain Feb 29.
3223+ // FEB_29_PERIOD_ONLY should use 365 days, FULL_LEAP_YEAR should use 366 days.
3224+ final RepaymentPeriod feb29Period2 = feb29Schedule .repaymentPeriods ().get (2 );
3225+ final RepaymentPeriod fullLeapPeriod2 = fullLeapSchedule .repaymentPeriods ().get (2 );
3226+ Assertions .assertNotEquals (toDouble (fullLeapPeriod2 .getDueInterest ()), toDouble (feb29Period2 .getDueInterest ()),
3227+ "Period without Feb 29 should differ between strategies (365 vs 366 days)" );
3228+ }
3229+
31773230 @ Test
31783231 public void test_leap_year_only_actual_no_effect_on_360_loan () {
31793232 final List <LoanScheduleModelRepaymentPeriod > expectedRepaymentPeriods = List .of (
0 commit comments