diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentApiConstants.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentApiConstants.java index 88392471d..dfef09099 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentApiConstants.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentApiConstants.java @@ -74,6 +74,8 @@ public final class InvestmentApiConstants { public static final String CP_BRANCH_NAME = "cpBranchName"; public static final String CP_ACCOUNT_NO = "cpAccountNo"; public static final String CP_IFSC = "cpIfsc"; + public static final String TDS_APPLICABLE = "tdsApplicable"; + public static final String TDS_RATE = "tdsRate"; // Script parameters public static final String SLR_TYPE = "slrType"; @@ -88,7 +90,7 @@ public final class InvestmentApiConstants { public static final Set PARTY_PARAMS = Set.of(BOND_TYPE, PARTY_NAME, ADDRESS1, ADDRESS2, ADDRESS3, CITY, PINCODE, PHONE, FAX, BANK_TYPE, GL_ACCOUNT_ID, INTEREST_GL_ACCOUNT_ID, RBI_CODE, EXPOSURE_LIMIT, LEI_NO, CP_BANK_NAME, CP_BRANCH_NAME, CP_ACCOUNT_NO, CP_IFSC, ACTIVE, - LOCALE); + TDS_APPLICABLE, TDS_RATE, LOCALE); public static final Set SCRIPT_PARAMS = Set.of(BOND_TYPE, SLR_TYPE, SCRIPT_DESC, GL_ACCOUNT_ID, ISIN, FACE_VALUE_PER_UNIT, INT_RATE, ISSUE_DATE, MATURITY_DATE, COMPANY_NAME, ADDRESS1, ADDRESS2, ADDRESS3, CITY, PINCODE, PHONE, FAX, CORP_CODE, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentFdApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentFdApiResource.java index 3179c7057..10b083945 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentFdApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentFdApiResource.java @@ -39,6 +39,7 @@ import org.apache.fineract.commands.domain.CommandWrapper; import org.apache.fineract.commands.service.CommandWrapperBuilder; import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.portfolio.investment.data.InvestmentFdClosureData; import org.apache.fineract.portfolio.investment.data.InvestmentFdData; @@ -135,9 +136,13 @@ public class InvestmentFdApiResource { @Operation(summary = "Preview a maturity, premature closure or renewal", description = "type MATURITY | PREMATURE | RENEWAL") public InvestmentFdClosureData closureQuote(@PathParam("fdId") final Long fdId, @QueryParam("type") final String type, @QueryParam("closeDate") final String closeDate, @QueryParam("receivedAmount") final BigDecimal receivedAmount, - @QueryParam("tds") final BigDecimal tds, @QueryParam("renewalAmount") final BigDecimal renewalAmount) { + @QueryParam("tds") final BigDecimal tds, @QueryParam("renewalAmount") final BigDecimal renewalAmount, + @QueryParam("principalBankGlAccountId") final Long principalBankGlAccountId, + @QueryParam("interestBankGlAccountId") final Long interestBankGlAccountId, + @QueryParam("diffBankGlAccountId") final Long diffBankGlAccountId) { this.context.authenticatedUser().validateHasReadPermission(InvestmentFdApiConstants.ENTITY); - return this.closureService.quote(fdId, type, date(closeDate), receivedAmount, tds, renewalAmount); + return this.closureService.quote(fdId, type, date(closeDate), receivedAmount, tds, renewalAmount, principalBankGlAccountId, + interestBankGlAccountId, diffBankGlAccountId); } @POST @@ -181,6 +186,14 @@ public class InvestmentFdApiResource { } private static LocalDate date(final String value) { - return StringUtils.isBlank(value) ? null : LocalDate.parse(value.trim(), DATE); + if (StringUtils.isBlank(value)) { + return null; + } + try { + return LocalDate.parse(value.trim(), DATE); + } catch (final java.time.format.DateTimeParseException e) { + // A half-typed date is a bad request, not a server error. + throw new GeneralPlatformDomainRuleException("error.msg.investmentfd.date.invalid", "Enter the date as DD-MM-YYYY", value); + } } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/data/InvestmentFdData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/data/InvestmentFdData.java index 0370324f5..31f205f64 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/data/InvestmentFdData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/data/InvestmentFdData.java @@ -39,6 +39,8 @@ public class InvestmentFdData { private final Long partyId; private final String partyCode; private final String partyName; + /** TDS rate the bank deducts on interest; null when it does not deduct TDS. */ + private final BigDecimal tdsRate; private final String certNo; private final String certDate; private final String dealDate; @@ -76,7 +78,9 @@ public class InvestmentFdData { public static InvestmentFdData from(final InvestmentFd f, final BigDecimal periodInterest) { return builder().id(f.getId()).dealNo(f.getDealNo()).partyId(f.getParty().getId()).partyCode(f.getParty().getPartyCode()) - .partyName(f.getParty().getPartyName()).certNo(f.getCertNo()).certDate(format(f.getCertDate())) + .partyName(f.getParty().getPartyName()) + .tdsRate(f.getParty().isTdsApplicable() ? f.getParty().getTdsRate() : null).certNo(f.getCertNo()) + .certDate(format(f.getCertDate())) .dealDate(format(f.getDealDate())).principal(f.getPrincipal()).intRate(f.getIntRate()) .maturityDate(format(f.getMaturityDate())).days(ChronoUnit.DAYS.between(f.getDealDate(), f.getMaturityDate())) .payout(f.getPayout()).calcType(f.getCalcType()).expectedInterest(f.getExpectedInterest()) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/data/InvestmentPartyData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/data/InvestmentPartyData.java index f4965a878..8a5c7f047 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/data/InvestmentPartyData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/data/InvestmentPartyData.java @@ -45,6 +45,8 @@ public class InvestmentPartyData { private final Long interestGlAccountId; private final String interestGlCode; private final String interestGlName; + private final boolean tdsApplicable; + private final BigDecimal tdsRate; private final String rbiCode; private final BigDecimal limit; private final String leiNo; @@ -62,7 +64,8 @@ public class InvestmentPartyData { .interestGlAccountId(p.getInterestGlAccount() == null ? null : p.getInterestGlAccount().getId()) .interestGlCode(p.getInterestGlAccount() == null ? null : p.getInterestGlAccount().getGlCode()) .interestGlName(p.getInterestGlAccount() == null ? null : p.getInterestGlAccount().getName()) - .rbiCode(p.getRbiCode()).limit(p.getExposureLimit()).leiNo(p.getLeiNo()).cpBankName(p.getCpBankName()) + .tdsApplicable(p.isTdsApplicable()).tdsRate(p.getTdsRate()).rbiCode(p.getRbiCode()).limit(p.getExposureLimit()) + .leiNo(p.getLeiNo()).cpBankName(p.getCpBankName()) .cpBranchName(p.getCpBranchName()).cpAccountNo(p.getCpAccountNo()).cpIfsc(p.getCpIfsc()).active(p.isActive()).build(); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/domain/InvestmentParty.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/domain/InvestmentParty.java index 2402e964a..043561d9e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/domain/InvestmentParty.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/domain/InvestmentParty.java @@ -83,6 +83,14 @@ public class InvestmentParty extends AbstractAuditableWithUTCDateTimeCustom InvestmentFdService.rule("not.found", "FD " + fdId + " does not exist")); final String t = CLOSURE_PREMATURE.equals(type) || CLOSURE_RENEWAL.equals(type) ? type : CLOSURE_MATURITY; final LocalDate date = CLOSURE_PREMATURE.equals(t) ? (closeDate == null ? DateUtils.getBusinessLocalDate() : closeDate) : f.getMaturityDate(); final Figures x = figures(f, t, date, receivedAmount, tds, renewalAmount); - final Entry e = CLOSURE_RENEWAL.equals(t) ? renewalEntry(f, x, f.getBankGlAccount()) : closureEntry(f, x, f.getBankGlAccount(), f.getBankGlAccount()); + // The banks the user has chosen, as the posting will use them; a renewal difference shows no bank line until one is chosen. + final GLAccount principalGl = principalBankGlId == null ? f.getBankGlAccount() : bankGl(principalBankGlId, f); + final GLAccount interestGl = interestBankGlId == null ? principalGl : bankGl(interestBankGlId, f); + final Entry e = CLOSURE_RENEWAL.equals(t) ? renewalEntry(f, x, diffBankGlId == null ? null : bankGl(diffBankGlId, f)) + : closureEntry(f, x, principalGl, interestGl); return x.toData(f, t, date, null, e); } @@ -328,7 +333,10 @@ public class InvestmentFdClosureService { } final BigDecimal expected = InvestmentFdCalculator.interest(f.getPrincipal(), f.getIntRate(), lastPaid, date, f.getCalcType(), f.getPayout()); - final BigDecimal tds = scale(zero(tdsIn)); + // TDS as entered; otherwise worked out at the bank's TDS rate, from the bank's net figure when given. + final BigDecimal tds = tdsIn != null ? scale(tdsIn) + : amountIn != null ? InvestmentFdService.tdsInNet(f.getParty(), scale(amountIn).subtract(f.getPrincipal())) + : InvestmentFdService.tdsOnGross(f.getParty(), expected); // Maturity / renewal default to the system's figure; a premature closure always needs the bank's amount. final BigDecimal received = amountIn != null ? scale(amountIn) : f.getPrincipal().add(expected).subtract(tds); final BigDecimal interest = received.add(tds).subtract(f.getPrincipal()); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentFdInterestService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentFdInterestService.java index 33e00c791..8eaaa348d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentFdInterestService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentFdInterestService.java @@ -176,11 +176,15 @@ public class InvestmentFdInterestService { x.setAmount(next.amount()); what = "FD interest provision"; } else { - // The bank's own figure when given (net of TDS), else the system's interest for the period. - final BigDecimal tds = amount(command, TDS); - final BigDecimal received = command.parameterExists(RECEIVED_AMOUNT) && command.bigDecimalValueOfParameterNamed(RECEIVED_AMOUNT) != null - ? amount(command, RECEIVED_AMOUNT) - : next.gross().subtract(tds); + // The bank's own figure when given (net of TDS), else the system's interest for the period. TDS as entered, else + // worked out at the bank's TDS rate. + final boolean bankFigure = command.parameterExists(RECEIVED_AMOUNT) + && command.bigDecimalValueOfParameterNamed(RECEIVED_AMOUNT) != null; + final boolean tdsGiven = command.parameterExists(TDS) && command.bigDecimalValueOfParameterNamed(TDS) != null; + final BigDecimal tds = tdsGiven ? amount(command, TDS) + : bankFigure ? InvestmentFdService.tdsInNet(f.getParty(), amount(command, RECEIVED_AMOUNT)) + : InvestmentFdService.tdsOnGross(f.getParty(), next.gross()); + final BigDecimal received = bankFigure ? amount(command, RECEIVED_AMOUNT) : next.gross().subtract(tds); final BigDecimal gross = received.add(tds); final BigDecimal income = gross.subtract(next.alreadyBooked()); final GLAccount bank = command.longValueOfParameterNamed(BANK_GL_ACCOUNT_ID) == null ? f.getBankGlAccount() @@ -260,7 +264,8 @@ public class InvestmentFdInterestService { final BigDecimal gross = done != null ? done.getGrossInterest() : InvestmentFdCalculator.interest(f.getPrincipal(), f.getIntRate(), lastPaid, d, CALC_SIMPLE, f.getPayout()); final BigDecimal booked = done != null ? zero(done.getAlreadyBooked()) : sincePaid; - out.add(new Line(type, d, lastPaid, gross, booked, done == null ? null : done.getTds(), + final BigDecimal tds = done == null ? InvestmentFdService.tdsOnGross(f.getParty(), gross) : done.getTds(); + out.add(new Line(type, d, lastPaid, gross, booked, tds, done != null ? done.getAmount() : gross.subtract(booked), done)); lastPaid = d; sincePaid = BigDecimal.ZERO; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentFdService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentFdService.java index 63b602911..33e1e517e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentFdService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentFdService.java @@ -287,6 +287,25 @@ public class InvestmentFdService { return PAYOUT_AT_MATURITY.equals(payout) ? amount.add(interest) : amount; } + /** TDS the bank deducts from a gross interest at the rate on its FD party; zero when the bank does not deduct TDS. */ + static BigDecimal tdsOnGross(final InvestmentParty party, final BigDecimal grossInterest) { + if (!party.isTdsApplicable() || party.getTdsRate() == null || grossInterest == null || grossInterest.signum() <= 0) { + return BigDecimal.ZERO; + } + return grossInterest.multiply(party.getTdsRate()).divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP); + } + + /** TDS already taken out of an interest the bank paid net of it: gross = net × 100 / (100 − rate), TDS = gross − net. */ + static BigDecimal tdsInNet(final InvestmentParty party, final BigDecimal netInterest) { + final BigDecimal hundred = BigDecimal.valueOf(100); + if (!party.isTdsApplicable() || party.getTdsRate() == null || party.getTdsRate().compareTo(hundred) >= 0 || netInterest == null + || netInterest.signum() <= 0) { + return BigDecimal.ZERO; + } + final BigDecimal gross = netInterest.multiply(hundred).divide(hundred.subtract(party.getTdsRate()), 2, RoundingMode.HALF_UP); + return gross.subtract(netInterest).setScale(2, RoundingMode.HALF_UP); + } + /** The 31 March of the previous financial year: for 16-10-2026 → 31-03-2026, for 31-03-2026 → 31-03-2025. */ static LocalDate previousYearEnd(final LocalDate date) { final int year = date.getMonthValue() >= 4 ? date.getYear() : date.getYear() - 1; 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 c5282eda8..bcd23e3e2 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 @@ -66,14 +66,14 @@ public final class InvestmentInterestCalculator { return (to.getYear() - from.getYear()) * 360 + (to.getMonthValue() - from.getMonthValue()) * 30 + (d2 - d1); } - /** Interest on {@code faceValue} at {@code ratePercent} p.a. for {@code days} (30/360), rounded to the rupee. */ + /** Interest on {@code faceValue} at {@code ratePercent} p.a. for {@code days} (30/360), to the paisa. */ public static BigDecimal interestForDays(final BigDecimal faceValue, final BigDecimal ratePercent, final int days) { - return faceValue.multiply(ratePercent).multiply(BigDecimal.valueOf(days)).divide(THIRTY_SIX_THOUSAND, 0, RoundingMode.HALF_UP); + return faceValue.multiply(ratePercent).multiply(BigDecimal.valueOf(days)).divide(THIRTY_SIX_THOUSAND, 2, RoundingMode.HALF_UP); } - /** The fixed half-yearly coupon: face value × rate ÷ 2. */ + /** The fixed half-yearly coupon: face value × rate ÷ 2, to the paisa. */ public static BigDecimal halfYearCoupon(final BigDecimal faceValue, final BigDecimal ratePercent) { - return faceValue.multiply(ratePercent).divide(TWO_HUNDRED, 0, RoundingMode.HALF_UP); + return faceValue.multiply(ratePercent).divide(TWO_HUNDRED, 2, RoundingMode.HALF_UP); } /** k-th coupon date counting back from maturity — measured from maturity each time so month-end days survive. */ diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentMasterWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentMasterWritePlatformServiceImpl.java index 833c6cb50..d114c4a76 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentMasterWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentMasterWritePlatformServiceImpl.java @@ -49,6 +49,8 @@ import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstant import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SCRIPT_CODE_PREFIX; import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SCRIPT_DESC; import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SLR_TYPE; +import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.TDS_APPLICABLE; +import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.TDS_RATE; import java.math.BigDecimal; import java.util.ArrayList; @@ -160,6 +162,19 @@ public class InvestmentMasterWritePlatformServiceImpl implements InvestmentMaste final Long interestGlId = command.longValueOfParameterNamed(INTEREST_GL_ACCOUNT_ID); p.setInterestGlAccount(interestGlId == null ? null : postableIncomeGl(interestGlId)); } + if (command.parameterExists(TDS_APPLICABLE)) { + p.setTdsApplicable(command.booleanPrimitiveValueOfParameterNamed(TDS_APPLICABLE)); + } + if (command.parameterExists(TDS_RATE)) { + p.setTdsRate(command.bigDecimalValueOfParameterNamed(TDS_RATE)); + } + // A bank that deducts TDS needs its rate; one that does not keeps no rate. + if (!p.isTdsApplicable()) { + p.setTdsRate(null); + } else if (p.getTdsRate() == null || p.getTdsRate().signum() <= 0) { + throw new GeneralPlatformDomainRuleException("error.msg.investment.party.tds.rate.required", + "Enter the TDS rate this bank deducts on FD interest"); + } } /** Interest on an FD with this bank is credited here, so it must be a usable Income account. */ diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml index 3ca2ec20c..f684a6ecb 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml @@ -184,4 +184,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0605_investment_party_tds.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0605_investment_party_tds.xml new file mode 100644 index 000000000..852f457cb --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0605_investment_party_tds.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + 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 916f4effb..ee6852750 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 @@ -60,16 +60,16 @@ class InvestmentInterestCalculatorTest { } @Test - void interestForDaysRoundsToTheRupee() { - assertEquals(new BigDecimal("120833"), InvestmentInterestCalculator.interestForDays(ONE_CRORE, RATE, 58)); - assertEquals(new BigDecimal("104167"), InvestmentInterestCalculator.interestForDays(ONE_CRORE, RATE, 50)); - assertEquals(new BigDecimal("375000"), InvestmentInterestCalculator.interestForDays(ONE_CRORE, RATE, 180)); + void interestForDaysIsToThePaisa() { + assertEquals(new BigDecimal("120833.33"), InvestmentInterestCalculator.interestForDays(ONE_CRORE, RATE, 58)); + assertEquals(new BigDecimal("104166.67"), InvestmentInterestCalculator.interestForDays(ONE_CRORE, RATE, 50)); + assertEquals(new BigDecimal("375000.00"), InvestmentInterestCalculator.interestForDays(ONE_CRORE, RATE, 180)); } @Test void halfYearCouponIsFaceValueTimesRateOverTwo() { - assertEquals(new BigDecimal("375000"), InvestmentInterestCalculator.halfYearCoupon(ONE_CRORE, RATE)); - assertEquals(new BigDecimal("362500"), InvestmentInterestCalculator.halfYearCoupon(ONE_CRORE, new BigDecimal("7.25"))); + assertEquals(new BigDecimal("375000.00"), InvestmentInterestCalculator.halfYearCoupon(ONE_CRORE, RATE)); + assertEquals(new BigDecimal("362500.00"), InvestmentInterestCalculator.halfYearCoupon(ONE_CRORE, new BigDecimal("7.25"))); } @Test @@ -106,14 +106,14 @@ class InvestmentInterestCalculatorTest { // 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 is income in full: the 1,20,833 paid to the seller was booked as an expense at purchase (RBI + // First coupon is income in full: the 1,20,833.33 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"); + assertRow(rows.get(0), TYPE_COUPON, LocalDate.of(2027, 2, 10), LocalDate.of(2026, 8, 11), 180, "375000.00", "0", "375000.00"); // 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"); + assertRow(rows.get(1), TYPE_PROVISION, LocalDate.of(2027, 3, 31), LocalDate.of(2027, 2, 11), 50, "104166.67", "0", "104166.67"); // August coupon clears the March provision. - assertRow(rows.get(2), TYPE_COUPON, LocalDate.of(2027, 8, 10), LocalDate.of(2027, 2, 11), 180, "375000", "104167", "270833"); - assertRow(rows.get(3), TYPE_PROVISION, LocalDate.of(2027, 9, 30), LocalDate.of(2027, 8, 11), 50, "104167", "0", "104167"); + assertRow(rows.get(2), TYPE_COUPON, LocalDate.of(2027, 8, 10), LocalDate.of(2027, 2, 11), 180, "375000.00", "104166.67", "270833.33"); + assertRow(rows.get(3), TYPE_PROVISION, LocalDate.of(2027, 9, 30), LocalDate.of(2027, 8, 11), 50, "104166.67", "0", "104166.67"); final ScheduleRow last = rows.get(rows.size() - 1); assertEquals(TYPE_COUPON, last.type()); @@ -121,7 +121,7 @@ class InvestmentInterestCalculatorTest { // 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)), totalIncome); + assertEquals(0, new BigDecimal("375000").multiply(BigDecimal.valueOf(16)).compareTo(totalIncome)); } @Test