feat(investment): bucket wise investment report by time left to maturity
This commit is contained in:
+66
@@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* 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.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.InvestmentBucketReportData;
|
||||||
|
import org.apache.fineract.portfolio.investment.service.InvestmentBucketReportService;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Path("/v1/investment/bucket-report")
|
||||||
|
@Component
|
||||||
|
@Tag(name = "Investment Bucket Wise", description = "Investments held on a date, split by the time left to maturity.")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class InvestmentBucketReportApiResource {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
||||||
|
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final InvestmentBucketReportService reportService;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Bucket Wise Investment", description = "asOnDate dd-MM-yyyy; default today")
|
||||||
|
public InvestmentBucketReportData report(@QueryParam("asOnDate") @Parameter(description = "dd-MM-yyyy") final String asOnDate) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(InvestmentPurchaseApiConstants.ENTITY);
|
||||||
|
if (asOnDate == null || asOnDate.isBlank()) {
|
||||||
|
return this.reportService.report(DateUtils.getBusinessLocalDate());
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return this.reportService.report(LocalDate.parse(asOnDate.trim(), DATE));
|
||||||
|
} catch (final DateTimeParseException e) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.investment.report.date.invalid", "Enter the date as DD-MM-YYYY",
|
||||||
|
asOnDate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+80
@@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bucket Wise Investment: Govt Securities, bonds and FDs held on a date, split by the time left to maturity (up to 1
|
||||||
|
* year, over 1 to 3, over 3 to 5, over 5 years).
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class InvestmentBucketReportData {
|
||||||
|
|
||||||
|
private final String asOnDate;
|
||||||
|
private final List<Bucket> buckets;
|
||||||
|
private final Bucket total;
|
||||||
|
private final List<Row> rows;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public static class Bucket {
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
private final String label;
|
||||||
|
private final int govtCount;
|
||||||
|
private final BigDecimal govtFaceValue;
|
||||||
|
private final BigDecimal govtBookValue;
|
||||||
|
private final int bondCount;
|
||||||
|
private final BigDecimal bondFaceValue;
|
||||||
|
private final BigDecimal bondBookValue;
|
||||||
|
private final int fdCount;
|
||||||
|
private final BigDecimal fdPrincipal;
|
||||||
|
/** Book value of securities and bonds plus FD principal. */
|
||||||
|
private final BigDecimal totalAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public static class Row {
|
||||||
|
|
||||||
|
/** Code of the bucket the holding falls in. */
|
||||||
|
private final String bucket;
|
||||||
|
/** GOVT_SECURITY, BOND (Non-SLR and other bonds) 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;
|
||||||
|
/** HTM / AFS / HFT of a security; blank for an FD. */
|
||||||
|
private final String category;
|
||||||
|
private final BigDecimal rate;
|
||||||
|
private final String maturityDate;
|
||||||
|
/** Days from the as-on date to maturity; negative when maturity has passed but it is not yet redeemed or closed. */
|
||||||
|
private final long daysToMaturity;
|
||||||
|
/** Face value held, or the FD principal. */
|
||||||
|
private final BigDecimal faceValue;
|
||||||
|
/** Cost less what sales, amortisation and write-offs took off, or the FD principal. */
|
||||||
|
private final BigDecimal bookValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
+153
@@ -0,0 +1,153 @@
|
|||||||
|
/**
|
||||||
|
* 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.time.temporal.ChronoUnit;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.portfolio.investment.api.InvestmentApiConstants;
|
||||||
|
import org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants;
|
||||||
|
import org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants;
|
||||||
|
import org.apache.fineract.portfolio.investment.data.InvestmentBucketReportData;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentFd;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentFdRepository;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bucket Wise Investment. Every Govt Security, bond and FD held on the as-on date goes into a bucket by the time left
|
||||||
|
* to its maturity; one already past maturity but not yet redeemed or closed counts in the first bucket.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class InvestmentBucketReportService {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
||||||
|
private static final String GOVT = "GOVT_SECURITY";
|
||||||
|
private static final String BOND = "BOND";
|
||||||
|
private static final String FD = "FD";
|
||||||
|
private static final String[][] BUCKETS = { { "UPTO_1Y", "Up to 1 year" }, { "1Y_3Y", "Over 1 year to 3 years" },
|
||||||
|
{ "3Y_5Y", "Over 3 years to 5 years" }, { "ABOVE_5Y", "Over 5 years" } };
|
||||||
|
|
||||||
|
private final InvestmentPurchaseRepository purchaseRepository;
|
||||||
|
private final InvestmentSaleRepository saleRepository;
|
||||||
|
private final InvestmentFdRepository fdRepository;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public InvestmentBucketReportData report(final LocalDate asOn) {
|
||||||
|
final List<InvestmentBucketReportData.Row> rows = new ArrayList<>();
|
||||||
|
for (final InvestmentPurchase p : this.purchaseRepository.findAll(Sort.by(Sort.Direction.ASC, "maturityDate", "id"))) {
|
||||||
|
security(p, asOn, rows);
|
||||||
|
}
|
||||||
|
for (final InvestmentFd f : this.fdRepository.findAll(Sort.by(Sort.Direction.ASC, "maturityDate", "id"))) {
|
||||||
|
final boolean held = !f.getDealDate().isAfter(asOn) && (f.getClosedOnDate() == null || f.getClosedOnDate().isAfter(asOn));
|
||||||
|
if (held) {
|
||||||
|
rows.add(row(asOn, FD, f.getId(), f.getDealNo(), f.getParty().getPartyName() + " · " + f.getCertNo(), null, f.getIntRate(),
|
||||||
|
f.getMaturityDate(), f.getPrincipal(), f.getPrincipal()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rows.sort(Comparator.comparingLong(InvestmentBucketReportData.Row::getDaysToMaturity));
|
||||||
|
|
||||||
|
final List<InvestmentBucketReportData.Bucket> buckets = new ArrayList<>();
|
||||||
|
for (final String[] b : BUCKETS) {
|
||||||
|
buckets.add(bucket(b[0], b[1], rows.stream().filter(r -> r.getBucket().equals(b[0])).toList()));
|
||||||
|
}
|
||||||
|
return InvestmentBucketReportData.builder().asOnDate(asOn.format(DATE)).buckets(buckets).total(bucket("TOTAL", "Total", rows))
|
||||||
|
.rows(rows).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A security counts when settled on or before the as-on date with units still held on it: units sold or redeemed
|
||||||
|
* after that date are added back, and book value is cost less what sales, amortisation and write-offs settled by
|
||||||
|
* then took off.
|
||||||
|
*/
|
||||||
|
private void security(final InvestmentPurchase p, final LocalDate asOn, final List<InvestmentBucketReportData.Row> rows) {
|
||||||
|
final String bondType = p.getBondType() != null ? p.getBondType() : p.getScript().getBondType();
|
||||||
|
if (!InvestmentApiConstants.DEBT_BOND_TYPES.contains(bondType) || p.getMaturityDate() == null
|
||||||
|
|| InvestmentPurchaseApiConstants.STATUS_DROPPED.equals(p.getStatus())
|
||||||
|
|| InvestmentPurchaseApiConstants.STATUS_PENDING_SETTLEMENT.equals(p.getStatus()) || p.getSettlementDate().isAfter(asOn)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BigDecimal units = p.getBalanceUnits();
|
||||||
|
BigDecimal book = p.getBookValue();
|
||||||
|
for (final InvestmentSale s : this.saleRepository.findByPurchaseIdOrderByIdAsc(p.getId())) {
|
||||||
|
if (!InvestmentSaleApiConstants.STATUS_SETTLED.equals(s.getStatus())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (s.getSettlementDate().isAfter(asOn)) {
|
||||||
|
units = units.add(s.getUnits());
|
||||||
|
} else {
|
||||||
|
book = book.subtract(s.getBookValueSold() == null ? BigDecimal.ZERO : s.getBookValueSold());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (units.signum() <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rows.add(row(asOn, GOVT.equals(bondType) ? GOVT : BOND, p.getId(), p.getDealNo(),
|
||||||
|
p.getScript().getScriptCode() + " " + p.getScript().getScriptDesc(), p.getCategory(), p.getIntRate(), p.getMaturityDate(),
|
||||||
|
units.multiply(p.getScript().getFaceValuePerUnit()), book));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InvestmentBucketReportData.Row row(final LocalDate asOn, final String instrument, final Long refId, final String dealNo,
|
||||||
|
final String particulars, final String category, final BigDecimal rate, final LocalDate maturity, final BigDecimal face,
|
||||||
|
final BigDecimal book) {
|
||||||
|
return InvestmentBucketReportData.Row.builder().bucket(bucketOf(asOn, maturity)).instrument(instrument).refId(refId).dealNo(dealNo)
|
||||||
|
.particulars(particulars).category(category).rate(rate).maturityDate(maturity.format(DATE))
|
||||||
|
.daysToMaturity(ChronoUnit.DAYS.between(asOn, maturity)).faceValue(face).bookValue(book).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String bucketOf(final LocalDate asOn, final LocalDate maturity) {
|
||||||
|
if (!maturity.isAfter(asOn.plusYears(1))) {
|
||||||
|
return BUCKETS[0][0];
|
||||||
|
}
|
||||||
|
if (!maturity.isAfter(asOn.plusYears(3))) {
|
||||||
|
return BUCKETS[1][0];
|
||||||
|
}
|
||||||
|
return maturity.isAfter(asOn.plusYears(5)) ? BUCKETS[3][0] : BUCKETS[2][0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InvestmentBucketReportData.Bucket bucket(final String code, final String label,
|
||||||
|
final List<InvestmentBucketReportData.Row> rows) {
|
||||||
|
final List<InvestmentBucketReportData.Row> govt = rows.stream().filter(r -> GOVT.equals(r.getInstrument())).toList();
|
||||||
|
final List<InvestmentBucketReportData.Row> bond = rows.stream().filter(r -> BOND.equals(r.getInstrument())).toList();
|
||||||
|
final List<InvestmentBucketReportData.Row> fd = rows.stream().filter(r -> FD.equals(r.getInstrument())).toList();
|
||||||
|
final BigDecimal govtBook = sum(govt, InvestmentBucketReportData.Row::getBookValue);
|
||||||
|
final BigDecimal bondBook = sum(bond, InvestmentBucketReportData.Row::getBookValue);
|
||||||
|
final BigDecimal fdPrincipal = sum(fd, InvestmentBucketReportData.Row::getBookValue);
|
||||||
|
return InvestmentBucketReportData.Bucket.builder().code(code).label(label).govtCount(govt.size())
|
||||||
|
.govtFaceValue(sum(govt, InvestmentBucketReportData.Row::getFaceValue)).govtBookValue(govtBook).bondCount(bond.size())
|
||||||
|
.bondFaceValue(sum(bond, InvestmentBucketReportData.Row::getFaceValue)).bondBookValue(bondBook).fdCount(fd.size())
|
||||||
|
.fdPrincipal(fdPrincipal).totalAmount(govtBook.add(bondBook).add(fdPrincipal)).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BigDecimal sum(final List<InvestmentBucketReportData.Row> rows,
|
||||||
|
final java.util.function.Function<InvestmentBucketReportData.Row, BigDecimal> f) {
|
||||||
|
return rows.stream().map(f).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user