diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/data/InvestmentInterestScheduleData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/data/InvestmentInterestScheduleData.java index 4e5dda5de..4ea5ba2ce 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/data/InvestmentInterestScheduleData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/data/InvestmentInterestScheduleData.java @@ -45,7 +45,9 @@ public class InvestmentInterestScheduleData { private final String dealDate; private final String lastIntDate; private final String maturityDate; - /** Interest paid to the seller at purchase — already in Interest Receivable. */ + /** + * Broken-period interest paid to the seller at purchase — booked as an expense (GL 500606), not in this schedule. + */ private final BigDecimal purchasedAccruedInterest; private final BigDecimal couponPerHalfYear; private final String asOnDate; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentInterestCalculator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentInterestCalculator.java index ef708d59d..038c4b3f5 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentInterestCalculator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentInterestCalculator.java @@ -35,6 +35,8 @@ import java.util.TreeSet; * Interest Receivable / Cr Interest Income), even though the cash arrives at the next coupon; *
  • when the coupon arrives, the provision already booked is cleared and only the rest is new income.
  • * + * Broken-period interest paid to the seller at purchase is an expense of its own (RBI UCB Investment Directions, para + * 152; GL 500606), so it is not carried here: the first coupon is income in full. */ public final class InvestmentInterestCalculator { @@ -129,11 +131,9 @@ public final class InvestmentInterestCalculator { * coupon date immediately before the purchase * @param dealDate * purchase deal date — closings before it belong to the seller - * @param purchasedAccrued - * interest paid to the seller at purchase; it already sits in Interest Receivable */ public static List schedule(final BigDecimal faceValue, final BigDecimal ratePercent, final LocalDate maturity, - final LocalDate lastIntDate, final LocalDate dealDate, final BigDecimal purchasedAccrued) { + final LocalDate lastIntDate, final LocalDate dealDate) { final List rows = new ArrayList<>(); if (faceValue == null || ratePercent == null || ratePercent.signum() <= 0 || maturity == null || lastIntDate == null || dealDate == null || !maturity.isAfter(lastIntDate)) { @@ -148,7 +148,7 @@ public final class InvestmentInterestCalculator { } final BigDecimal coupon = halfYearCoupon(faceValue, ratePercent); - BigDecimal receivable = purchasedAccrued == null ? BigDecimal.ZERO : purchasedAccrued; + BigDecimal receivable = BigDecimal.ZERO; LocalDate previousCoupon = lastIntDate; for (final LocalDate date : events) { final int days = days360(previousCoupon, date); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentPurchaseReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentPurchaseReadPlatformService.java index 71f00343c..fc63727ac 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentPurchaseReadPlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentPurchaseReadPlatformService.java @@ -66,7 +66,7 @@ public class InvestmentPurchaseReadPlatformService { final BigDecimal rate = p.getIntRate() == null ? BigDecimal.ZERO : p.getIntRate(); final List rows = InvestmentInterestCalculator - .schedule(faceHeld, rate, p.getMaturityDate(), p.getLastIntDate(), p.getDealDate(), p.getAccruedInterest()).stream() + .schedule(faceHeld, rate, p.getMaturityDate(), p.getLastIntDate(), p.getDealDate()).stream() .map(r -> InvestmentInterestScheduleData.Row.builder().type(r.type()).intDate(format(r.date())) .fromDate(format(r.fromDate())).toDate(format(r.toDate())).days(r.days()).grossInterest(r.grossInterest()) .alreadyBooked(r.alreadyBooked()).amount(r.amount()).due(!r.date().isAfter(asOn)).build()) diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/investment/service/InvestmentInterestCalculatorTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/investment/service/InvestmentInterestCalculatorTest.java index 398da9445..916f4effb 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/investment/service/InvestmentInterestCalculatorTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/investment/service/InvestmentInterestCalculatorTest.java @@ -100,15 +100,15 @@ class InvestmentInterestCalculatorTest { } @Test - void scheduleClearsPurchasedInterestAndProvisionsAtEachClosing() { - final List rows = InvestmentInterestCalculator.schedule(ONE_CRORE, RATE, MATURITY, LAST_COUPON, DEAL, - new BigDecimal("120833")); + void scheduleProvisionsAtEachClosingAndClearsAtTheCoupon() { + final List rows = InvestmentInterestCalculator.schedule(ONE_CRORE, RATE, MATURITY, LAST_COUPON, DEAL); // 16 coupons + 15 closings (Mar 2027 … Mar 2034); 30-09-2026 is before the deal, so it is the seller's. assertEquals(31, rows.size()); - // First coupon: 1,20,833 paid to the seller is recovered, only the rest is our income. - assertRow(rows.get(0), TYPE_COUPON, LocalDate.of(2027, 2, 10), LocalDate.of(2026, 8, 11), 180, "375000", "120833", "254167"); + // First coupon is income in full: the 1,20,833 paid to the seller was booked as an expense at purchase (RBI + // para 152). + assertRow(rows.get(0), TYPE_COUPON, LocalDate.of(2027, 2, 10), LocalDate.of(2026, 8, 11), 180, "375000", "0", "375000"); // 31 March closing: 50 days earned since 10 Feb, nothing booked yet. assertRow(rows.get(1), TYPE_PROVISION, LocalDate.of(2027, 3, 31), LocalDate.of(2027, 2, 11), 50, "104167", "0", "104167"); // August coupon clears the March provision. @@ -119,25 +119,25 @@ class InvestmentInterestCalculatorTest { assertEquals(TYPE_COUPON, last.type()); assertEquals(MATURITY, last.date()); - // Over the life, income booked = all coupons − interest bought from the seller. + // Over the life, income booked = all the coupons. final BigDecimal totalIncome = rows.stream().map(ScheduleRow::amount).reduce(BigDecimal.ZERO, BigDecimal::add); - assertEquals(new BigDecimal("375000").multiply(BigDecimal.valueOf(16)).subtract(new BigDecimal("120833")), totalIncome); + assertEquals(new BigDecimal("375000").multiply(BigDecimal.valueOf(16)), totalIncome); } @Test void closingOnACouponDateIsNotProvisionedTwice() { // Coupons on 31 Mar / 30 Sep: every closing is a coupon day, so there are no provision lines. final List rows = InvestmentInterestCalculator.schedule(ONE_CRORE, RATE, LocalDate.of(2028, 3, 31), - LocalDate.of(2026, 9, 30), DEAL, BigDecimal.ZERO); + LocalDate.of(2026, 9, 30), DEAL); assertEquals(3, rows.size()); assertTrue(rows.stream().allMatch(r -> TYPE_COUPON.equals(r.type()))); } @Test void scheduleIsEmptyWithoutRateOrDates() { - assertTrue(InvestmentInterestCalculator.schedule(ONE_CRORE, BigDecimal.ZERO, MATURITY, LAST_COUPON, DEAL, null).isEmpty()); - assertTrue(InvestmentInterestCalculator.schedule(ONE_CRORE, RATE, null, LAST_COUPON, DEAL, null).isEmpty()); - assertTrue(InvestmentInterestCalculator.schedule(ONE_CRORE, RATE, MATURITY, null, DEAL, null).isEmpty()); + assertTrue(InvestmentInterestCalculator.schedule(ONE_CRORE, BigDecimal.ZERO, MATURITY, LAST_COUPON, DEAL).isEmpty()); + assertTrue(InvestmentInterestCalculator.schedule(ONE_CRORE, RATE, null, LAST_COUPON, DEAL).isEmpty()); + assertTrue(InvestmentInterestCalculator.schedule(ONE_CRORE, RATE, MATURITY, null, DEAL).isEmpty()); } @Test