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