feat(investment): total interest received and receivable report
This commit is contained in:
+79
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.investment.api;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import java.time.LocalDate;
|
||||
import java.time.Month;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.investment.data.InvestmentInterestReportData;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentInterestReportService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Path("/v1/investment/interest-report")
|
||||
@Component
|
||||
@Tag(name = "Investment Interest Received & Receivable", description = "Interest received in a period and receivable at its end.")
|
||||
@RequiredArgsConstructor
|
||||
public class InvestmentInterestReportApiResource {
|
||||
|
||||
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final InvestmentInterestReportService reportService;
|
||||
|
||||
@GET
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Total Interest Received & Receivable", description = "fromDate / toDate dd-MM-yyyy; default 1 April of the "
|
||||
+ "current financial year to today")
|
||||
public InvestmentInterestReportData report(@QueryParam("fromDate") @Parameter(description = "dd-MM-yyyy") final String fromDate,
|
||||
@QueryParam("toDate") @Parameter(description = "dd-MM-yyyy") final String toDate) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(InvestmentPurchaseApiConstants.ENTITY);
|
||||
final LocalDate today = DateUtils.getBusinessLocalDate();
|
||||
final LocalDate fyStart = LocalDate.of(today.getMonthValue() >= 4 ? today.getYear() : today.getYear() - 1, Month.APRIL, 1);
|
||||
final LocalDate from = date(fromDate, fyStart);
|
||||
final LocalDate to = date(toDate, today);
|
||||
if (to.isBefore(from)) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.investment.report.dates", "The To date cannot be before the From date");
|
||||
}
|
||||
return this.reportService.report(from, to);
|
||||
}
|
||||
|
||||
private static LocalDate date(final String value, final LocalDate fallback) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return fallback;
|
||||
}
|
||||
try {
|
||||
return LocalDate.parse(value.trim(), DATE);
|
||||
} catch (final DateTimeParseException e) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.investment.report.date.invalid", "Enter the date as DD-MM-YYYY", value);
|
||||
}
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.investment.data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* Total Interest Received & Receivable: per Govt Security / bond deal and FD, the interest received in a period and
|
||||
* the interest earned up to its last day that has not come in yet.
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
public class InvestmentInterestReportData {
|
||||
|
||||
private final String fromDate;
|
||||
private final String toDate;
|
||||
private final BigDecimal totalReceived;
|
||||
private final BigDecimal totalReceivable;
|
||||
private final BigDecimal totalProvisioned;
|
||||
private final BigDecimal total;
|
||||
private final List<Row> rows;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
public static class Row {
|
||||
|
||||
/** SECURITY or FD. */
|
||||
private final String instrument;
|
||||
private final Long refId;
|
||||
private final String dealNo;
|
||||
/** Script, or bank and certificate of an FD. */
|
||||
private final String particulars;
|
||||
private final String status;
|
||||
/** Face value held, or the FD principal. */
|
||||
private final BigDecimal principal;
|
||||
private final BigDecimal rate;
|
||||
/** Interest that came in during the period (coupons, FD payouts, interest paid at closure), gross of TDS. */
|
||||
private final BigDecimal received;
|
||||
/** Interest earned up to the To date and not yet received. */
|
||||
private final BigDecimal receivable;
|
||||
/** Part of the receivable already booked by 31 Mar / 30 Sep provisions (Interest Receivable 100603). */
|
||||
private final BigDecimal provisioned;
|
||||
private final BigDecimal total;
|
||||
}
|
||||
}
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.investment.service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants;
|
||||
import org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants;
|
||||
import org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants;
|
||||
import org.apache.fineract.portfolio.investment.data.InvestmentInterestReportData;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentFd;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentFdInterest;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentFdInterestRepository;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentFdRepository;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentInterestPosting;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentInterestPostingRepository;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchase;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchaseRepository;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentSale;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentSaleRepository;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* Total Interest Received & Receivable. Received: interest that came in during the period. Receivable: interest
|
||||
* earned up to the To date and not received yet — coupons / payouts already due but not posted, plus what has accrued
|
||||
* since the last one — with the part already booked by the 31 Mar / 30 Sep provisions shown beside it.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class InvestmentInterestReportService {
|
||||
|
||||
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
||||
private static final String COUPON = "COUPON";
|
||||
private static final String PROVISION = "PROVISION";
|
||||
|
||||
private final InvestmentPurchaseRepository purchaseRepository;
|
||||
private final InvestmentInterestPostingRepository postingRepository;
|
||||
private final InvestmentSaleRepository saleRepository;
|
||||
private final InvestmentPurchaseReadPlatformService purchaseReadPlatformService;
|
||||
private final InvestmentFdRepository fdRepository;
|
||||
private final InvestmentFdInterestRepository fdInterestRepository;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public InvestmentInterestReportData report(final LocalDate from, final LocalDate to) {
|
||||
final List<InvestmentInterestReportData.Row> rows = new ArrayList<>();
|
||||
for (final InvestmentPurchase p : this.purchaseRepository.findAll(Sort.by(Sort.Direction.ASC, "id"))) {
|
||||
if (InvestmentPurchaseApiConstants.STATUS_DROPPED.equals(p.getStatus())
|
||||
|| InvestmentPurchaseApiConstants.STATUS_PENDING_SETTLEMENT.equals(p.getStatus())) {
|
||||
continue;
|
||||
}
|
||||
security(p, from, to, rows);
|
||||
}
|
||||
for (final InvestmentFd f : this.fdRepository.findAll(Sort.by(Sort.Direction.ASC, "id"))) {
|
||||
fd(f, from, to, rows);
|
||||
}
|
||||
final BigDecimal received = sum(rows, InvestmentInterestReportData.Row::getReceived);
|
||||
final BigDecimal receivable = sum(rows, InvestmentInterestReportData.Row::getReceivable);
|
||||
return InvestmentInterestReportData.builder().fromDate(from.format(DATE)).toDate(to.format(DATE)).totalReceived(received)
|
||||
.totalReceivable(receivable).totalProvisioned(sum(rows, InvestmentInterestReportData.Row::getProvisioned))
|
||||
.total(received.add(receivable)).rows(rows).build();
|
||||
}
|
||||
|
||||
private void security(final InvestmentPurchase p, final LocalDate from, final LocalDate to,
|
||||
final List<InvestmentInterestReportData.Row> rows) {
|
||||
final List<InvestmentInterestPosting> posted = this.postingRepository.findByPurchaseIdOrderByIntDateAsc(p.getId());
|
||||
BigDecimal received = BigDecimal.ZERO;
|
||||
BigDecimal provisioned = BigDecimal.ZERO;
|
||||
LocalDate lastCouponPosted = null;
|
||||
for (final InvestmentInterestPosting x : posted) {
|
||||
if (COUPON.equals(x.getLineType()) && inPeriod(x.getIntDate(), from, to)) {
|
||||
received = received.add(zero(x.getGrossInterest()));
|
||||
}
|
||||
if (!x.getIntDate().isAfter(to)) {
|
||||
// A coupon clears what the provisions before it booked; each provision adds the interest earned since.
|
||||
if (COUPON.equals(x.getLineType())) {
|
||||
provisioned = BigDecimal.ZERO;
|
||||
lastCouponPosted = x.getIntDate();
|
||||
} else if (PROVISION.equals(x.getLineType())) {
|
||||
provisioned = provisioned.add(zero(x.getAmount()));
|
||||
}
|
||||
}
|
||||
}
|
||||
// A sale clears the receivable share of the units sold; units sold after the To date were still held on it.
|
||||
BigDecimal soldAfterTo = BigDecimal.ZERO;
|
||||
for (final InvestmentSale s : this.saleRepository.findByPurchaseIdOrderByIdAsc(p.getId())) {
|
||||
if (!InvestmentSaleApiConstants.STATUS_SETTLED.equals(s.getStatus())) {
|
||||
continue;
|
||||
}
|
||||
if (s.getSettlementDate().isAfter(to)) {
|
||||
soldAfterTo = soldAfterTo.add(s.getUnits());
|
||||
} else if (lastCouponPosted == null || s.getSettlementDate().isAfter(lastCouponPosted)) {
|
||||
provisioned = provisioned.subtract(zero(s.getReceivableCleared()));
|
||||
}
|
||||
}
|
||||
provisioned = provisioned.max(BigDecimal.ZERO);
|
||||
|
||||
BigDecimal receivable = BigDecimal.ZERO;
|
||||
final BigDecimal face = p.getBalanceUnits().add(soldAfterTo).multiply(p.getScript().getFaceValuePerUnit());
|
||||
final boolean heldOnTo = InvestmentPurchaseApiConstants.STATUS_ACTIVE.equals(p.getStatus()) || soldAfterTo.signum() > 0;
|
||||
if (heldOnTo && !p.getSettlementDate().isAfter(to)) {
|
||||
final BigDecimal rate = p.getIntRate() == null ? BigDecimal.ZERO : p.getIntRate();
|
||||
LocalDate lastCoupon = p.getLastIntDate();
|
||||
for (final InvestmentInterestCalculator.ScheduleRow r : this.purchaseReadPlatformService.scheduleRows(p)) {
|
||||
if (!COUPON.equals(r.type()) || r.date().isAfter(to)) {
|
||||
continue;
|
||||
}
|
||||
lastCoupon = r.date();
|
||||
final boolean done = posted.stream().anyMatch(x -> COUPON.equals(x.getLineType()) && x.getIntDate().equals(r.date()));
|
||||
if (!done) {
|
||||
receivable = receivable.add(r.grossInterest());
|
||||
}
|
||||
}
|
||||
if (lastCoupon != null && rate.signum() > 0) {
|
||||
receivable = receivable.add(
|
||||
InvestmentInterestCalculator.interestForDays(face, rate, InvestmentInterestCalculator.days360(lastCoupon, to)));
|
||||
}
|
||||
} else {
|
||||
provisioned = BigDecimal.ZERO;
|
||||
}
|
||||
add(rows, "SECURITY", p.getId(), p.getDealNo(), p.getScript().getScriptCode() + " " + p.getScript().getScriptDesc(), p.getStatus(),
|
||||
face.signum() > 0 ? face : p.getFaceValue(), p.getIntRate(), received, receivable, provisioned);
|
||||
}
|
||||
|
||||
private void fd(final InvestmentFd f, final LocalDate from, final LocalDate to, final List<InvestmentInterestReportData.Row> rows) {
|
||||
final List<InvestmentFdInterest> posted = this.fdInterestRepository.findByFdIdOrderByIntDateAscIdAsc(f.getId());
|
||||
BigDecimal received = BigDecimal.ZERO;
|
||||
BigDecimal provisioned = BigDecimal.ZERO;
|
||||
LocalDate lastPaid = f.getDealDate();
|
||||
for (final InvestmentFdInterest x : posted) {
|
||||
final boolean payout = InvestmentFdApiConstants.LINE_PAYOUT.equals(x.getLineType());
|
||||
if (payout && inPeriod(x.getIntDate(), from, to)) {
|
||||
received = received.add(zero(x.getGrossInterest()));
|
||||
}
|
||||
if (!x.getIntDate().isAfter(to)) {
|
||||
if (payout) {
|
||||
provisioned = provisioned.subtract(zero(x.getAlreadyBooked()));
|
||||
lastPaid = x.getIntDate();
|
||||
} else if (InvestmentFdApiConstants.LINE_PROVISION.equals(x.getLineType())) {
|
||||
provisioned = provisioned.add(zero(x.getAmount()));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (f.getClosedOnDate() != null && inPeriod(f.getClosedOnDate(), from, to)) {
|
||||
received = received.add(zero(f.getClosureInterest()));
|
||||
}
|
||||
|
||||
BigDecimal receivable = BigDecimal.ZERO;
|
||||
final boolean openOnTo = !f.getDealDate().isAfter(to) && (f.getClosedOnDate() == null || f.getClosedOnDate().isAfter(to));
|
||||
if (openOnTo) {
|
||||
final LocalDate end = f.getMaturityDate().isBefore(to) ? f.getMaturityDate() : to;
|
||||
receivable = InvestmentFdCalculator.interest(f.getPrincipal(), f.getIntRate(), lastPaid, end, f.getCalcType(), f.getPayout());
|
||||
} else {
|
||||
provisioned = BigDecimal.ZERO;
|
||||
}
|
||||
add(rows, "FD", f.getId(), f.getDealNo(), f.getParty().getPartyName() + " · " + f.getCertNo(), f.getStatus(), f.getPrincipal(),
|
||||
f.getIntRate(), received, receivable, provisioned.max(BigDecimal.ZERO));
|
||||
}
|
||||
|
||||
private static void add(final List<InvestmentInterestReportData.Row> rows, final String instrument, final Long refId,
|
||||
final String dealNo, final String particulars, final String status, final BigDecimal principal, final BigDecimal rate,
|
||||
final BigDecimal received, final BigDecimal receivable, final BigDecimal provisioned) {
|
||||
if (received.signum() == 0 && receivable.signum() == 0) {
|
||||
return;
|
||||
}
|
||||
rows.add(InvestmentInterestReportData.Row.builder().instrument(instrument).refId(refId).dealNo(dealNo).particulars(particulars)
|
||||
.status(status).principal(principal).rate(rate).received(received).receivable(receivable).provisioned(provisioned)
|
||||
.total(received.add(receivable)).build());
|
||||
}
|
||||
|
||||
private static boolean inPeriod(final LocalDate d, final LocalDate from, final LocalDate to) {
|
||||
return d != null && !d.isBefore(from) && !d.isAfter(to);
|
||||
}
|
||||
|
||||
private static BigDecimal zero(final BigDecimal v) {
|
||||
return v == null ? BigDecimal.ZERO : v;
|
||||
}
|
||||
|
||||
private static BigDecimal sum(final List<InvestmentInterestReportData.Row> rows,
|
||||
final java.util.function.Function<InvestmentInterestReportData.Row, BigDecimal> f) {
|
||||
return rows.stream().map(f).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user