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