feat: add investment portfolio module with support for fixed deposits, shares, valuation, and purchase management
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Waiting to run
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Waiting to run
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Waiting to run
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Waiting to run
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Waiting to run
Fineract Documentation build / build (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Waiting to run
Fineract Liquibase Only mode - PostgreSQL / test (push) Waiting to run
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Waiting to run
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Waiting to run
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Waiting to run
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Waiting to run
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Waiting to run
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Waiting to run
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Waiting to run
Fineract Documentation build / build (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Waiting to run
Fineract Liquibase Only mode - PostgreSQL / test (push) Waiting to run
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Waiting to run
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Waiting to run
This commit is contained in:
+8
@@ -263,6 +263,14 @@ public class CommandWrapperBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder dividendInvestmentPurchase(final Long purchaseId) {
|
||||
this.actionName = "DIVIDEND";
|
||||
this.entityName = "INVESTMENTPURCHASE";
|
||||
this.entityId = purchaseId;
|
||||
this.href = "/investment/purchases/" + purchaseId + "/dividends";
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder createInvestmentValuation() {
|
||||
this.actionName = "CREATE";
|
||||
this.entityName = "INVESTMENTVALUATION";
|
||||
|
||||
+18
-1
@@ -71,6 +71,19 @@ public final class InvestmentApiConstants {
|
||||
/** RBI: only debt and money-market mutual funds; their sub types start with this. */
|
||||
public static final String DEBT_MF_SUB_TYPE_PREFIX = "DEBT";
|
||||
|
||||
/** Share investments: only these institutions (no shares of companies outside the co-operative sector). */
|
||||
public static final String SHARE = "SHARE";
|
||||
public static final String INSTITUTION_STATE_COOP_BANK = "STATE_COOP_BANK";
|
||||
public static final String INSTITUTION_DCCB = "DCCB";
|
||||
public static final String INSTITUTION_OTHER_COOP_SOCIETY = "OTHER_COOP_SOCIETY";
|
||||
/** Membership shares of NPCI, CCIL, SWIFT, the Umbrella Organisation of UCBs and IDPIC. */
|
||||
public static final String INSTITUTION_MEMBERSHIP_MIC_UO = "MEMBERSHIP_MIC_UO";
|
||||
public static final Set<String> INSTITUTION_TYPES = Set.of(INSTITUTION_STATE_COOP_BANK, INSTITUTION_DCCB, INSTITUTION_OTHER_COOP_SOCIETY,
|
||||
INSTITUTION_MEMBERSHIP_MIC_UO);
|
||||
/** Co-operative institutions: their shares are valued on the dividend / balance sheet rules. */
|
||||
public static final Set<String> COOP_INSTITUTION_TYPES = Set.of(INSTITUTION_STATE_COOP_BANK, INSTITUTION_DCCB,
|
||||
INSTITUTION_OTHER_COOP_SOCIETY);
|
||||
|
||||
public static final String PARTY_CODE_PREFIX = "PTY-";
|
||||
|
||||
// Common parameters
|
||||
@@ -100,6 +113,10 @@ public final class InvestmentApiConstants {
|
||||
public static final String CP_IFSC = "cpIfsc";
|
||||
public static final String TDS_APPLICABLE = "tdsApplicable";
|
||||
public static final String TDS_RATE = "tdsRate";
|
||||
public static final String INSTITUTION_TYPE = "institutionType";
|
||||
public static final String SUBSCRIBED_CAPITAL = "subscribedCapital";
|
||||
public static final String LAST_BALANCE_SHEET_DATE = "lastBalanceSheetDate";
|
||||
public static final String IN_LIQUIDATION = "inLiquidation";
|
||||
|
||||
// Script parameters
|
||||
public static final String SLR_TYPE = "slrType";
|
||||
@@ -117,7 +134,7 @@ public final class InvestmentApiConstants {
|
||||
|
||||
public static final Set<String> 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,
|
||||
TDS_APPLICABLE, TDS_RATE, LOCALE);
|
||||
TDS_APPLICABLE, TDS_RATE, INSTITUTION_TYPE, SUBSCRIBED_CAPITAL, LAST_BALANCE_SHEET_DATE, IN_LIQUIDATION, LOCALE, DATE_FORMAT);
|
||||
|
||||
public static final Set<String> 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,
|
||||
|
||||
+2
-1
@@ -70,6 +70,7 @@ public final class InvestmentFdApiConstants {
|
||||
public static final String REMARKS = "remarks";
|
||||
public static final String AS_ON_DATE = "asOnDate";
|
||||
public static final String TOTAL_DEPOSITS = "totalDeposits";
|
||||
public static final String OWNED_FUNDS = "ownedFunds";
|
||||
public static final String LOCALE = "locale";
|
||||
public static final String DATE_FORMAT = "dateFormat";
|
||||
|
||||
@@ -115,5 +116,5 @@ public final class InvestmentFdApiConstants {
|
||||
|
||||
public static final Set<String> FD_PARAMS = Set.of(PARTY_ID, CERT_NO, CERT_DATE, DEAL_DATE, AMOUNT, INT_RATE, MATURITY_DATE, PAYOUT,
|
||||
CALC_TYPE, BANK_GL_ACCOUNT_ID, REMARKS, LOCALE, DATE_FORMAT);
|
||||
public static final Set<String> BASE_PARAMS = Set.of(AS_ON_DATE, TOTAL_DEPOSITS, REMARKS, LOCALE, DATE_FORMAT);
|
||||
public static final Set<String> BASE_PARAMS = Set.of(AS_ON_DATE, TOTAL_DEPOSITS, OWNED_FUNDS, REMARKS, LOCALE, DATE_FORMAT);
|
||||
}
|
||||
|
||||
+8
@@ -73,4 +73,12 @@ public final class InvestmentPurchaseApiConstants {
|
||||
public static final Set<String> PARAMS = Set.of(BOND_TYPE, SCRIPT_ID, PARTY_ID, DEAL_DATE, DEAL_TIME, SETTLEMENT_TYPE, FACE_VALUE,
|
||||
PURCHASE_RATE, CONSIDER_AMT, NAV, STAMP_DUTY, BROKERAGE, OTHER_CHARGES, CATEGORY, LAST_INT_DATE, CR_GL_ACCOUNT_ID, SUB_TYPE,
|
||||
OTHER_DETAILS, LOCALE, DATE_FORMAT);
|
||||
|
||||
/** Dividend received on a share holding: Dr Bank / Cr 400404 Dividend on Investments, dated the day it is received. */
|
||||
public static final String DIVIDEND_GL_CODE = "400404";
|
||||
public static final String DIVIDEND_DATE = "dividendDate";
|
||||
public static final String AMOUNT = "amount";
|
||||
public static final String BANK_GL_ACCOUNT_ID = "bankGlAccountId";
|
||||
public static final String REMARKS = "remarks";
|
||||
public static final Set<String> DIVIDEND_PARAMS = Set.of(DIVIDEND_DATE, AMOUNT, BANK_GL_ACCOUNT_ID, REMARKS, LOCALE, DATE_FORMAT);
|
||||
}
|
||||
|
||||
+40
@@ -41,13 +41,16 @@ import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformS
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.investment.data.InvestmentDividendData;
|
||||
import org.apache.fineract.portfolio.investment.data.InvestmentInterestScheduleData;
|
||||
import org.apache.fineract.portfolio.investment.data.InvestmentNonSlrLimitData;
|
||||
import org.apache.fineract.portfolio.investment.data.InvestmentPurchaseData;
|
||||
import org.apache.fineract.portfolio.investment.data.InvestmentSaleData;
|
||||
import org.apache.fineract.portfolio.investment.data.InvestmentShareLimitData;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentNonSlrService;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentPurchaseReadPlatformService;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentSaleReadPlatformService;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentShareService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Path("/v1/investment/purchases")
|
||||
@@ -61,6 +64,7 @@ public class InvestmentPurchaseApiResource {
|
||||
private final InvestmentSaleReadPlatformService saleReadPlatformService;
|
||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||
private final InvestmentNonSlrService nonSlrService;
|
||||
private final InvestmentShareService shareService;
|
||||
|
||||
@GET
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@@ -83,6 +87,29 @@ public class InvestmentPurchaseApiResource {
|
||||
return this.nonSlrService.limits(date, scriptId, amount);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("share-limit")
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "RBI limits for a share purchase", description = "Where a purchase of amount (book value) of shares of partyId "
|
||||
+ "on dealDate (dd-MM-yyyy, default today) stands against the 2% of owned funds and 5% of the society's capital limits; "
|
||||
+ "membership shares are exempt.")
|
||||
public InvestmentShareLimitData shareLimit(@QueryParam("partyId") @Parameter(description = "partyId") final Long partyId,
|
||||
@QueryParam("dealDate") @Parameter(description = "dd-MM-yyyy") final String dealDate,
|
||||
@QueryParam("amount") @Parameter(description = "amount") final BigDecimal amount) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(InvestmentPurchaseApiConstants.ENTITY);
|
||||
final LocalDate date = InvestmentApiDates.parse(dealDate, DateUtils.getBusinessLocalDate(), "deal date");
|
||||
return this.shareService.limits(date, partyId, amount);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("dividends")
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Dividends received on share holdings", description = "Newest first, with their vouchers.")
|
||||
public List<InvestmentDividendData> retrieveDividends() {
|
||||
this.context.authenticatedUser().validateHasReadPermission(InvestmentPurchaseApiConstants.ENTITY);
|
||||
return this.shareService.retrieveDividends();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{purchaseId}")
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@@ -176,6 +203,19 @@ public class InvestmentPurchaseApiResource {
|
||||
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("{purchaseId}/dividends")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Record a dividend received on a share holding", description = "dividendDate, amount, bankGlAccountId, "
|
||||
+ "remarks: Dr the bank GL / Cr 400404 Dividend on Investments, dated the day it was received.")
|
||||
public CommandProcessingResult postDividend(@PathParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId,
|
||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
final CommandWrapper request = new CommandWrapperBuilder().dividendInvestmentPurchase(purchaseId).withJson(apiRequestBodyAsJson)
|
||||
.build();
|
||||
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("{purchaseId}/transfer")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
|
||||
+2
@@ -35,6 +35,8 @@ public final class InvestmentValuationApiConstants {
|
||||
public static final String CLASS_GOVT = "GOVT_SECURITY";
|
||||
public static final String CLASS_BOND = "CORPORATE_BOND";
|
||||
public static final String CLASS_FUND = "MUTUAL_FUND";
|
||||
/** Shares are valued by rule, not at a market rate: face value, nil, or ₹1 per institution (see the valuation service). */
|
||||
public static final String CLASS_SHARE = "SHARE";
|
||||
|
||||
/** Depreciation charged to P&L (Provisions & Contingencies) and the reserve it is held in. */
|
||||
public static final String DEPRECIATION_PROVISION_GL_CODE = "500504";
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 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.time.format.DateTimeFormatter;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentDividend;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchase;
|
||||
|
||||
/** A dividend received on a share holding, with its voucher. */
|
||||
@Getter
|
||||
@Builder
|
||||
public class InvestmentDividendData {
|
||||
|
||||
private final Long id;
|
||||
private final Long purchaseId;
|
||||
private final String dealNo;
|
||||
private final String certNo;
|
||||
private final String scriptCode;
|
||||
private final String scriptDesc;
|
||||
private final Long partyId;
|
||||
private final String partyName;
|
||||
private final String dividendDate;
|
||||
private final BigDecimal amount;
|
||||
private final String bankGlCode;
|
||||
private final String bankGlName;
|
||||
private final String remarks;
|
||||
private final String glTransactionId;
|
||||
private final String voucherNo;
|
||||
|
||||
public static InvestmentDividendData from(final InvestmentDividend d) {
|
||||
final InvestmentPurchase p = d.getPurchase();
|
||||
return InvestmentDividendData.builder().id(d.getId()).purchaseId(p.getId()).dealNo(p.getDealNo()).certNo(p.getCertNo())
|
||||
.scriptCode(p.getScript().getScriptCode()).scriptDesc(p.getScript().getScriptDesc()).partyId(p.getParty().getId())
|
||||
.partyName(p.getParty().getPartyName()).dividendDate(d.getDividendDate().format(DateTimeFormatter.ofPattern("dd-MM-yyyy")))
|
||||
.amount(d.getAmount()).bankGlCode(d.getBankGlAccount().getGlCode()).bankGlName(d.getBankGlAccount().getName())
|
||||
.remarks(d.getRemarks()).glTransactionId(d.getGlTransactionId()).voucherNo(d.getVoucherNo()).build();
|
||||
}
|
||||
}
|
||||
+2
@@ -31,6 +31,8 @@ public class InvestmentFdLimitData {
|
||||
private final String asOnDate;
|
||||
/** Total deposits as on that date, as entered; empty until someone enters it. */
|
||||
private final BigDecimal totalDeposits;
|
||||
/** Owned funds (paid-up share capital + reserves) as on that date, the base of the 2% limit on co-operative shares. */
|
||||
private final BigDecimal ownedFunds;
|
||||
/** The same figure worked out from the deposit GLs, as a suggestion. */
|
||||
private final BigDecimal glTotalDeposits;
|
||||
private final BigDecimal grossLimit;
|
||||
|
||||
+10
-1
@@ -19,6 +19,7 @@
|
||||
package org.apache.fineract.portfolio.investment.data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentParty;
|
||||
@@ -54,6 +55,10 @@ public class InvestmentPartyData {
|
||||
private final String cpBranchName;
|
||||
private final String cpAccountNo;
|
||||
private final String cpIfsc;
|
||||
private final String institutionType;
|
||||
private final BigDecimal subscribedCapital;
|
||||
private final String lastBalanceSheetDate;
|
||||
private final boolean inLiquidation;
|
||||
private final boolean active;
|
||||
|
||||
public static InvestmentPartyData from(final InvestmentParty p) {
|
||||
@@ -66,6 +71,10 @@ public class InvestmentPartyData {
|
||||
.interestGlName(p.getInterestGlAccount() == null ? null : p.getInterestGlAccount().getName())
|
||||
.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();
|
||||
.cpBranchName(p.getCpBranchName()).cpAccountNo(p.getCpAccountNo()).cpIfsc(p.getCpIfsc())
|
||||
.institutionType(p.getInstitutionType()).subscribedCapital(p.getSubscribedCapital())
|
||||
.lastBalanceSheetDate(p.getLastBalanceSheetDate() == null ? null
|
||||
: p.getLastBalanceSheetDate().format(DateTimeFormatter.ofPattern("dd-MM-yyyy")))
|
||||
.inLiquidation(p.isInLiquidation()).active(p.isActive()).build();
|
||||
}
|
||||
}
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 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 lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* Where a share purchase stands against the RBI limits: shares of co-operative societies within 2% of owned funds, and
|
||||
* within 5% of one society's subscribed capital. Membership shares (DCCB, State Co-op Bank, NPCI / CCIL / SWIFT, Umbrella
|
||||
* Organisation, IDPIC) may exceed them.
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
public class InvestmentShareLimitData {
|
||||
|
||||
/** The 31 March the owned funds are taken from. */
|
||||
private final String asOnDate;
|
||||
private final String institutionType;
|
||||
/** True for membership shares, which the limits do not bind. */
|
||||
private final boolean exempt;
|
||||
private final BigDecimal ownedFunds;
|
||||
private final BigDecimal coopLimit;
|
||||
private final BigDecimal coopOutstanding;
|
||||
private final BigDecimal coopAfter;
|
||||
private final String partyName;
|
||||
private final BigDecimal subscribedCapital;
|
||||
private final BigDecimal institutionLimit;
|
||||
private final BigDecimal institutionOutstanding;
|
||||
private final BigDecimal institutionAfter;
|
||||
private final BigDecimal newAmount;
|
||||
private final boolean withinLimits;
|
||||
private final String message;
|
||||
}
|
||||
+3
@@ -81,5 +81,8 @@ public class InvestmentValuationData {
|
||||
private final String rateDate;
|
||||
private final BigDecimal marketValue;
|
||||
private final BigDecimal difference;
|
||||
/** How a share was valued; empty for a holding valued at its market rate. */
|
||||
private final String valuationBasis;
|
||||
private final boolean npi;
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -42,6 +42,10 @@ public class InvestmentDepositBase extends AbstractAuditableWithUTCDateTimeCusto
|
||||
@Column(name = "total_deposits", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal totalDeposits;
|
||||
|
||||
/** Owned funds (paid-up share capital + reserves) on the same date: shares of co-operative societies are limited to 2% of it. */
|
||||
@Column(name = "owned_funds", scale = 6, precision = 19)
|
||||
private BigDecimal ownedFunds;
|
||||
|
||||
@Column(name = "remarks", length = 500)
|
||||
private String remarks;
|
||||
}
|
||||
|
||||
+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.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
|
||||
/** A dividend received on a share holding: booked as income when it is received (Dr Bank / Cr Dividend on Investments). */
|
||||
@Entity
|
||||
@Table(name = "m_investment_dividend")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class InvestmentDividend extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "purchase_id", nullable = false)
|
||||
private InvestmentPurchase purchase;
|
||||
|
||||
@Column(name = "dividend_date", nullable = false)
|
||||
private LocalDate dividendDate;
|
||||
|
||||
@Column(name = "amount", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal amount;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "bank_gl_account_id", nullable = false)
|
||||
private GLAccount bankGlAccount;
|
||||
|
||||
@Column(name = "remarks", length = 500)
|
||||
private String remarks;
|
||||
|
||||
@Column(name = "gl_transaction_id", length = 50)
|
||||
private String glTransactionId;
|
||||
|
||||
@Column(name = "voucher_no", length = 30)
|
||||
private String voucherNo;
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 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.domain;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface InvestmentDividendRepository extends JpaRepository<InvestmentDividend, Long> {
|
||||
|
||||
List<InvestmentDividend> findAllByOrderByDividendDateDescIdDesc();
|
||||
|
||||
/** Whether the institution has paid us any dividend on or before a date (on any of our holdings of its shares). */
|
||||
boolean existsByPurchasePartyIdAndDividendDateLessThanEqual(Long partyId, LocalDate date);
|
||||
}
|
||||
+17
@@ -24,6 +24,7 @@ import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
@@ -112,6 +113,22 @@ public class InvestmentParty extends AbstractAuditableWithUTCDateTimeCustom<Long
|
||||
@Column(name = "cp_ifsc", length = 11)
|
||||
private String cpIfsc;
|
||||
|
||||
/** Share institutions only: DCCB, State Co-op Bank, other co-operative society, or MIC / Umbrella Organisation. */
|
||||
@Column(name = "institution_type", length = 30)
|
||||
private String institutionType;
|
||||
|
||||
/** The institution's subscribed share capital: our holding in a co-operative society is limited to 5% of it. */
|
||||
@Column(name = "subscribed_capital", scale = 6, precision = 19)
|
||||
private BigDecimal subscribedCapital;
|
||||
|
||||
/** Date of the institution's latest balance sheet we have; one older than 18 months values its shares at ₹1. */
|
||||
@Column(name = "last_balance_sheet_date")
|
||||
private LocalDate lastBalanceSheetDate;
|
||||
|
||||
/** An institution in liquidation: its shares are fully provided for. */
|
||||
@Column(name = "is_in_liquidation", nullable = false)
|
||||
private boolean inLiquidation;
|
||||
|
||||
/** An inactive party is kept for history but not offered for new deals. */
|
||||
@Column(name = "is_active", nullable = false)
|
||||
private boolean active = true;
|
||||
|
||||
+3
@@ -55,6 +55,9 @@ public class InvestmentValuation extends AbstractAuditableWithUTCDateTimeCustom<
|
||||
@Column(name = "fund_required", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal fundRequired;
|
||||
|
||||
@Column(name = "share_required", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal shareRequired;
|
||||
|
||||
@Column(name = "required_total", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal requiredTotal;
|
||||
|
||||
|
||||
+8
@@ -72,4 +72,12 @@ public class InvestmentValuationLine extends AbstractPersistableCustom<Long> {
|
||||
/** Market value less book value: a loss when negative. */
|
||||
@Column(name = "difference", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal difference;
|
||||
|
||||
/** How a share was valued (face value, nil, ₹1); empty for a holding valued at its market rate. */
|
||||
@Column(name = "valuation_basis", length = 100)
|
||||
private String valuationBasis;
|
||||
|
||||
/** A non-performing investment: shares of an institution whose latest balance sheet is over 18 months old. */
|
||||
@Column(name = "is_npi", nullable = false)
|
||||
private boolean npi;
|
||||
}
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 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.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentShareService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "INVESTMENTPURCHASE", action = "DIVIDEND")
|
||||
@RequiredArgsConstructor
|
||||
public class DividendInvestmentPurchaseCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final InvestmentShareService shareService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.shareService.postDividend(command.entityId(), command);
|
||||
}
|
||||
}
|
||||
+20
@@ -36,8 +36,11 @@ import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstant
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.FACE_VALUE_PER_UNIT;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.FAX;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.GL_ACCOUNT_ID;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.INSTITUTION_TYPE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.INSTITUTION_TYPES;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.INTEREST_GL_ACCOUNT_ID;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.INT_RATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.LAST_BALANCE_SHEET_DATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ISIN;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ISSUE_DATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.LEI_NO;
|
||||
@@ -59,6 +62,7 @@ import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstant
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SCRIPT_PARAMS;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SLR_TYPE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SLR_TYPES;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SUBSCRIBED_CAPITAL;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.TDS_RATE;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
@@ -77,6 +81,7 @@ import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/** Validates Bank / Party Master and Script Master requests before anything is written. */
|
||||
@@ -133,6 +138,21 @@ public final class InvestmentMasterCommandFromApiJsonDeserializer {
|
||||
final BigDecimal rate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(TDS_RATE, element);
|
||||
v.reset().parameter(TDS_RATE).value(rate).ignoreIfNull().positiveAmount().notGreaterThanMax(BigDecimal.valueOf(100));
|
||||
}
|
||||
final String institutionType = this.fromApiJsonHelper.extractStringNamed(INSTITUTION_TYPE, element);
|
||||
if (StringUtils.isNotBlank(institutionType)) {
|
||||
v.reset().parameter(INSTITUTION_TYPE).value(institutionType).isOneOfTheseValues(INSTITUTION_TYPES.toArray());
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists(SUBSCRIBED_CAPITAL, element)) {
|
||||
final BigDecimal capital = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(SUBSCRIBED_CAPITAL, element);
|
||||
v.reset().parameter(SUBSCRIBED_CAPITAL).value(capital).ignoreIfNull().positiveAmount();
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists(LAST_BALANCE_SHEET_DATE, element)) {
|
||||
final LocalDate balanceSheet = this.fromApiJsonHelper.extractLocalDateNamed(LAST_BALANCE_SHEET_DATE, element);
|
||||
if (balanceSheet != null && balanceSheet.isAfter(DateUtils.getBusinessLocalDate())) {
|
||||
v.reset().parameter(LAST_BALANCE_SHEET_DATE).failWithCodeAndMessage("in.future",
|
||||
"The balance sheet date cannot be after today");
|
||||
}
|
||||
}
|
||||
throwIfErrors(errors);
|
||||
}
|
||||
|
||||
|
||||
+18
-3
@@ -43,6 +43,7 @@ import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConsta
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.RD_DEPOSIT_GL_CODE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.REMARKS;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.STATUS_ACTIVE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.OWNED_FUNDS;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.TOTAL_DEPOSITS;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
@@ -145,7 +146,9 @@ public class InvestmentFdService {
|
||||
@Transactional(readOnly = true)
|
||||
public InvestmentFdLimitData limits(final LocalDate dealDate, final InvestmentParty party, final BigDecimal amount) {
|
||||
final LocalDate base = previousYearEnd(dealDate);
|
||||
final BigDecimal total = this.depositBaseRepository.findByAsOnDate(base).map(InvestmentDepositBase::getTotalDeposits).orElse(null);
|
||||
final InvestmentDepositBase depositBase = this.depositBaseRepository.findByAsOnDate(base).orElse(null);
|
||||
final BigDecimal total = depositBase == null ? null : depositBase.getTotalDeposits();
|
||||
final BigDecimal ownedFunds = depositBase == null ? null : depositBase.getOwnedFunds();
|
||||
final BigDecimal newAmount = amount == null ? BigDecimal.ZERO : amount;
|
||||
final BigDecimal fdOutstanding = this.fdRepository.findByStatusOrderByIdAsc(STATUS_ACTIVE).stream().map(InvestmentFd::getPrincipal)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
@@ -156,7 +159,7 @@ public class InvestmentFdService {
|
||||
final BigDecimal grossAfter = fdOutstanding.add(bankBalances).add(newAmount);
|
||||
final BigDecimal bankAfter = withParty.add(newAmount);
|
||||
final InvestmentFdLimitData.InvestmentFdLimitDataBuilder b = InvestmentFdLimitData.builder().asOnDate(base.format(DATE))
|
||||
.totalDeposits(total).glTotalDeposits(glDeposits(base)).fdOutstanding(fdOutstanding).bankBalances(bankBalances)
|
||||
.totalDeposits(total).ownedFunds(ownedFunds).glTotalDeposits(glDeposits(base)).fdOutstanding(fdOutstanding).bankBalances(bankBalances)
|
||||
.grossAfter(grossAfter).partyName(party == null ? null : party.getPartyName()).bankAfter(bankAfter).newAmount(newAmount);
|
||||
if (total == null) {
|
||||
return b.withinLimits(false).message("Enter the bank's total deposits as on " + base.format(DATE)
|
||||
@@ -257,7 +260,9 @@ public class InvestmentFdService {
|
||||
validate(command.json(), BASE_PARAMS, v -> {
|
||||
final JsonElement e = this.fromApiJsonHelper.parse(command.json());
|
||||
v.reset().parameter(AS_ON_DATE).value(this.fromApiJsonHelper.extractLocalDateNamed(AS_ON_DATE, e)).notNull();
|
||||
v.reset().parameter(TOTAL_DEPOSITS).value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(TOTAL_DEPOSITS, e)).notNull()
|
||||
v.reset().parameter(TOTAL_DEPOSITS).value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(TOTAL_DEPOSITS, e))
|
||||
.ignoreIfNull().positiveAmount();
|
||||
v.reset().parameter(OWNED_FUNDS).value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(OWNED_FUNDS, e)).ignoreIfNull()
|
||||
.positiveAmount();
|
||||
v.reset().parameter(REMARKS).value(this.fromApiJsonHelper.extractStringNamed(REMARKS, e)).ignoreIfNull().notExceedingLengthOf(500);
|
||||
});
|
||||
@@ -267,7 +272,16 @@ public class InvestmentFdService {
|
||||
}
|
||||
final InvestmentDepositBase base = this.depositBaseRepository.findByAsOnDate(asOn).orElseGet(InvestmentDepositBase::new);
|
||||
base.setAsOnDate(asOn);
|
||||
// Owned funds may be added later (Share Entry) to a year whose total deposits are already saved.
|
||||
if (command.bigDecimalValueOfParameterNamed(TOTAL_DEPOSITS) != null) {
|
||||
base.setTotalDeposits(command.bigDecimalValueOfParameterNamed(TOTAL_DEPOSITS));
|
||||
}
|
||||
if (base.getTotalDeposits() == null) {
|
||||
throw rule("base.deposits.required", "Enter the total deposits as on " + asOn.format(DATE) + " as well");
|
||||
}
|
||||
if (command.parameterExists(OWNED_FUNDS)) {
|
||||
base.setOwnedFunds(command.bigDecimalValueOfParameterNamed(OWNED_FUNDS));
|
||||
}
|
||||
base.setRemarks(StringUtils.trimToNull(command.stringValueOfParameterNamed(REMARKS)));
|
||||
final InvestmentDepositBase saved = this.depositBaseRepository.saveAndFlush(base);
|
||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(saved.getId())
|
||||
@@ -335,6 +349,7 @@ public class InvestmentFdService {
|
||||
}
|
||||
|
||||
String nextDealNo(final LocalDate dealDate) {
|
||||
InvestmentPurchaseWritePlatformServiceImpl.lockNumbering(this.jdbcTemplate, "m_investment_fd.deal_no");
|
||||
final String prefix = InvestmentPurchaseWritePlatformServiceImpl.dealNoPrefix(dealDate) + "F";
|
||||
final Integer max = this.jdbcTemplate.queryForObject(
|
||||
"SELECT MAX(CAST(SUBSTRING(deal_no FROM 8) AS INTEGER)) FROM m_investment_fd WHERE deal_no LIKE ?", Integer.class,
|
||||
|
||||
+3
-1
@@ -68,8 +68,10 @@ public class InvestmentInterestReportService {
|
||||
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"))) {
|
||||
// Shares earn dividends, not interest.
|
||||
if (InvestmentPurchaseApiConstants.STATUS_DROPPED.equals(p.getStatus())
|
||||
|| InvestmentPurchaseApiConstants.STATUS_PENDING_SETTLEMENT.equals(p.getStatus())) {
|
||||
|| InvestmentPurchaseApiConstants.STATUS_PENDING_SETTLEMENT.equals(p.getStatus())
|
||||
|| InvestmentApiConstants.SHARE.equals(p.getBondType())) {
|
||||
continue;
|
||||
}
|
||||
security(p, from, to, rows);
|
||||
|
||||
+45
@@ -35,10 +35,14 @@ import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstant
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.FACE_VALUE_PER_UNIT;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.FAX;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.GL_ACCOUNT_ID;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.INSTITUTION_OTHER_COOP_SOCIETY;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.INSTITUTION_TYPE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.INTEREST_GL_ACCOUNT_ID;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.INT_RATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.IN_LIQUIDATION;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ISIN;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ISSUE_DATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.LAST_BALANCE_SHEET_DATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.LEI_NO;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.LISTED;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.MATURITY_DATE;
|
||||
@@ -51,7 +55,9 @@ import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstant
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.RBI_CODE;
|
||||
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.SHARE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SLR_TYPE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SUBSCRIBED_CAPITAL;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.TDS_APPLICABLE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.TDS_RATE;
|
||||
|
||||
@@ -176,6 +182,7 @@ public class InvestmentMasterWritePlatformServiceImpl implements InvestmentMaste
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.investmentparty.name.duplicate",
|
||||
"A " + party.getBondType() + " party named " + party.getPartyName() + " already exists", party.getPartyName());
|
||||
}
|
||||
requireShareInstitutionRules(party);
|
||||
if (!"FD".equals(party.getBondType())) {
|
||||
return;
|
||||
}
|
||||
@@ -193,6 +200,30 @@ public class InvestmentMasterWritePlatformServiceImpl implements InvestmentMaste
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A share party is the institution whose shares the bank holds: its type decides the limits and the valuation, and a
|
||||
* co-operative society needs its subscribed capital for the 5% limit. Other parties carry none of this.
|
||||
*/
|
||||
private static void requireShareInstitutionRules(final InvestmentParty party) {
|
||||
if (!SHARE.equals(party.getBondType())) {
|
||||
party.setInstitutionType(null);
|
||||
party.setSubscribedCapital(null);
|
||||
party.setLastBalanceSheetDate(null);
|
||||
party.setInLiquidation(false);
|
||||
return;
|
||||
}
|
||||
if (StringUtils.isBlank(party.getInstitutionType())) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.investmentparty.institution.type.required",
|
||||
"Choose the institution type: DCCB, State Co-op Bank, co-operative society, or NPCI / CCIL / SWIFT / "
|
||||
+ "Umbrella Organisation; a UCB may not hold shares of other companies");
|
||||
}
|
||||
if (INSTITUTION_OTHER_COOP_SOCIETY.equals(party.getInstitutionType())
|
||||
&& (party.getSubscribedCapital() == null || party.getSubscribedCapital().signum() <= 0)) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.investmentparty.subscribed.capital.required",
|
||||
"Enter the society's subscribed share capital; the bank may hold at most 5% of it");
|
||||
}
|
||||
}
|
||||
|
||||
/** Copies every party field present in the request onto the entity (absent fields are left unchanged). */
|
||||
private void applyParty(final InvestmentParty p, final JsonCommand command) {
|
||||
setIfPresent(command, BOND_TYPE, p::setBondType);
|
||||
@@ -224,6 +255,18 @@ public class InvestmentMasterWritePlatformServiceImpl implements InvestmentMaste
|
||||
final Long interestGlId = command.longValueOfParameterNamed(INTEREST_GL_ACCOUNT_ID);
|
||||
p.setInterestGlAccount(interestGlId == null ? null : postableIncomeGl(interestGlId));
|
||||
}
|
||||
if (command.parameterExists(INSTITUTION_TYPE)) {
|
||||
p.setInstitutionType(StringUtils.trimToNull(command.stringValueOfParameterNamedAllowingNull(INSTITUTION_TYPE)));
|
||||
}
|
||||
if (command.parameterExists(SUBSCRIBED_CAPITAL)) {
|
||||
p.setSubscribedCapital(command.bigDecimalValueOfParameterNamed(SUBSCRIBED_CAPITAL));
|
||||
}
|
||||
if (command.parameterExists(LAST_BALANCE_SHEET_DATE)) {
|
||||
p.setLastBalanceSheetDate(command.localDateValueOfParameterNamed(LAST_BALANCE_SHEET_DATE));
|
||||
}
|
||||
if (command.parameterExists(IN_LIQUIDATION)) {
|
||||
p.setInLiquidation(command.booleanPrimitiveValueOfParameterNamed(IN_LIQUIDATION));
|
||||
}
|
||||
if (command.parameterExists(TDS_APPLICABLE)) {
|
||||
p.setTdsApplicable(command.booleanPrimitiveValueOfParameterNamed(TDS_APPLICABLE));
|
||||
}
|
||||
@@ -260,6 +303,7 @@ public class InvestmentMasterWritePlatformServiceImpl implements InvestmentMaste
|
||||
|
||||
/** Next code in the PTY-001, PTY-002 … series. */
|
||||
private String nextPartyCode() {
|
||||
InvestmentPurchaseWritePlatformServiceImpl.lockNumbering(this.jdbcTemplate, "m_investment_party.party_code");
|
||||
final Integer max = this.jdbcTemplate.queryForObject(
|
||||
"SELECT MAX(CAST(SUBSTRING(party_code FROM 5) AS INTEGER)) FROM m_investment_party WHERE party_code ~ '^PTY-[0-9]+$'",
|
||||
Integer.class);
|
||||
@@ -423,6 +467,7 @@ public class InvestmentMasterWritePlatformServiceImpl implements InvestmentMaste
|
||||
|
||||
/** Next code for the bond type — its letter plus a five-digit running number, e.g. G00001. */
|
||||
private String nextScriptCode(final String bondType) {
|
||||
InvestmentPurchaseWritePlatformServiceImpl.lockNumbering(this.jdbcTemplate, "m_investment_script.script_code");
|
||||
final String prefix = SCRIPT_CODE_PREFIX.get(bondType);
|
||||
final Integer max = this.jdbcTemplate.queryForObject(
|
||||
"SELECT MAX(CAST(SUBSTRING(script_code FROM 2) AS INTEGER)) FROM m_investment_script WHERE script_code ~ ?",
|
||||
|
||||
+2
-1
@@ -115,7 +115,8 @@ public class InvestmentNonSlrService {
|
||||
BigDecimal unlistedOutstanding = BigDecimal.ZERO;
|
||||
BigDecimal nonSlrBase = BigDecimal.ZERO;
|
||||
for (final InvestmentPurchase p : this.purchaseRepository.findAll()) {
|
||||
if (!NON_SLR_BOND_TYPES.contains(p.getBondType())) {
|
||||
// Shares of co-operative societies are non-SLR too; membership shares (DCCB, StCB, NPCI, ...) may exceed the limit.
|
||||
if (!NON_SLR_BOND_TYPES.contains(p.getBondType()) && !InvestmentShareService.societyShare(p)) {
|
||||
continue;
|
||||
}
|
||||
final BigDecimal now = bookNow(p);
|
||||
|
||||
+2
-1
@@ -135,7 +135,8 @@ public class InvestmentPurchaseReadPlatformService {
|
||||
/** The holding's interest schedule (from its settlement date to maturity) on the face value still held. */
|
||||
public List<InvestmentInterestCalculator.ScheduleRow> scheduleRows(final InvestmentPurchase p) {
|
||||
// Non-SLR (bonds, mutual funds) has no interest scheme (Jitendra Sir): its income is the gain on sale / redemption.
|
||||
if (InvestmentApiConstants.NON_SLR_BOND_TYPES.contains(p.getBondType())) {
|
||||
// Shares earn dividends, booked when received; they have no interest schedule either.
|
||||
if (InvestmentApiConstants.NON_SLR_BOND_TYPES.contains(p.getBondType()) || InvestmentApiConstants.SHARE.equals(p.getBondType())) {
|
||||
return List.of();
|
||||
}
|
||||
final BigDecimal rate = p.getIntRate() == null ? BigDecimal.ZERO : p.getIntRate();
|
||||
|
||||
+56
-1
@@ -61,7 +61,9 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.portfolio.investment.api.InvestmentApiConstants;
|
||||
import org.apache.fineract.portfolio.investment.data.InvestmentNonSlrLimitData;
|
||||
import org.apache.fineract.portfolio.investment.data.InvestmentShareLimitData;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentParty;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentPartyRepository;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchase;
|
||||
@@ -94,6 +96,7 @@ public class InvestmentPurchaseWritePlatformServiceImpl implements InvestmentPur
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final InvestmentJournalPoster journalPoster;
|
||||
private final InvestmentNonSlrService nonSlrService;
|
||||
private final InvestmentShareService shareService;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@@ -127,6 +130,17 @@ public class InvestmentPurchaseWritePlatformServiceImpl implements InvestmentPur
|
||||
throw rule("party.fd", "Party " + party.getPartyCode() + " " + party.getPartyName()
|
||||
+ " is an FD bank; choose a dealer / counterparty party for a " + bondType + " purchase");
|
||||
}
|
||||
final boolean share = InvestmentApiConstants.SHARE.equals(bondType);
|
||||
if (share) {
|
||||
// Only shares of a DCCB / State Co-op Bank, a co-operative society or a membership institution (NPCI, CCIL, ...).
|
||||
if (!InvestmentApiConstants.SHARE.equals(party.getBondType()) || StringUtils.isBlank(party.getInstitutionType())) {
|
||||
throw rule("share.party", "Party " + party.getPartyCode() + " " + party.getPartyName() + " is not a share institution; "
|
||||
+ "add it in Bank / Party Master with Bond Type SHARE and its institution type");
|
||||
}
|
||||
if (!StringUtils.startsWith(command.stringValueOfParameterNamed(CATEGORY), "AFS")) {
|
||||
throw rule("share.category", "Shares are held as AFS (Available For Sale)");
|
||||
}
|
||||
}
|
||||
final boolean nonSlr = NON_SLR_BOND_TYPES.contains(bondType);
|
||||
if (nonSlr) {
|
||||
this.nonSlrService.requireEligible(script, command.stringValueOfParameterNamedAllowingNull(SUB_TYPE));
|
||||
@@ -182,11 +196,20 @@ public class InvestmentPurchaseWritePlatformServiceImpl implements InvestmentPur
|
||||
computeMutualFund(p, command);
|
||||
} else if (nonSlr) {
|
||||
computeNonSlrBond(p, command, script);
|
||||
} else if (share) {
|
||||
computeShare(p, command, script);
|
||||
} else {
|
||||
computeSecurity(p, command, script);
|
||||
}
|
||||
p.setSettlementAmount(p.getBookValue().add(p.getAccruedInterest()).add(totalCharges(p)));
|
||||
if (nonSlr) {
|
||||
if (share) {
|
||||
final InvestmentShareLimitData shareLimit = this.shareService.limits(dealDate, party, p.getBookValue());
|
||||
if (!shareLimit.isWithinLimits()) {
|
||||
throw rule("share.limit.exceeded", shareLimit.getMessage());
|
||||
}
|
||||
}
|
||||
// Shares of co-operative societies are non-SLR investments too; membership shares may exceed the limits.
|
||||
if (nonSlr || InvestmentShareService.societyShare(p)) {
|
||||
final InvestmentNonSlrLimitData limit = this.nonSlrService.limits(dealDate, script, p.getBookValue());
|
||||
if (!limit.isWithinLimits()) {
|
||||
throw rule(limit.getTotalDeposits() == null ? "nonslr.deposit.base.missing" : "nonslr.limit.exceeded", limit.getMessage());
|
||||
@@ -333,6 +356,28 @@ public class InvestmentPurchaseWritePlatformServiceImpl implements InvestmentPur
|
||||
p.setAccruedInterest(BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shares: face value (number of shares x face value per share) at a price per share — co-operative shares are taken at
|
||||
* face value. No interest; the dividend is income when it is received.
|
||||
*/
|
||||
private void computeShare(final InvestmentPurchase p, final JsonCommand command, final InvestmentScript script) {
|
||||
final BigDecimal faceValue = command.bigDecimalValueOfParameterNamed(FACE_VALUE);
|
||||
final BigDecimal price = command.bigDecimalValueOfParameterNamed(PURCHASE_RATE);
|
||||
if (faceValue.remainder(script.getFaceValuePerUnit()).signum() != 0) {
|
||||
throw rule("facevalue.not.multiple", "Face value must be a whole number of shares of "
|
||||
+ script.getFaceValuePerUnit().stripTrailingZeros().toPlainString() + " each of " + script.getScriptCode());
|
||||
}
|
||||
final BigDecimal shares = faceValue.divide(script.getFaceValuePerUnit(), 6, RoundingMode.HALF_UP);
|
||||
p.setFaceValue(faceValue);
|
||||
p.setPurchaseRate(price);
|
||||
p.setUnits(shares);
|
||||
p.setBalanceUnits(shares);
|
||||
p.setBookValue(shares.multiply(price).setScale(2, RoundingMode.HALF_UP));
|
||||
p.setPremiumDiscount(BigDecimal.ZERO);
|
||||
p.setAccruedDays(0);
|
||||
p.setAccruedInterest(BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
/** Mutual fund: the amount invested buys units at the NAV. */
|
||||
private void computeMutualFund(final InvestmentPurchase p, final JsonCommand command) {
|
||||
final BigDecimal amount = command.bigDecimalValueOfParameterNamed(CONSIDER_AMT);
|
||||
@@ -367,6 +412,7 @@ public class InvestmentPurchaseWritePlatformServiceImpl implements InvestmentPur
|
||||
* by a mutual fund switch, which opens a new deal.
|
||||
*/
|
||||
static String nextDealNo(final JdbcTemplate jdbcTemplate, final LocalDate dealDate) {
|
||||
lockNumbering(jdbcTemplate, "m_investment_purchase.deal_no");
|
||||
final String prefix = dealNoPrefix(dealDate);
|
||||
final Integer max = jdbcTemplate.queryForObject(
|
||||
"SELECT MAX(CAST(SUBSTRING(deal_no FROM 7) AS INTEGER)) FROM m_investment_purchase WHERE deal_no LIKE ?", Integer.class,
|
||||
@@ -380,7 +426,16 @@ public class InvestmentPurchaseWritePlatformServiceImpl implements InvestmentPur
|
||||
return fyStart + String.format("%02d", (fyStart + 1) % 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds a lock on a number series until the transaction ends, so that two commands approved together (the Checker
|
||||
* Inbox sends a bulk approve in parallel) do not read the same MAX and take the same number.
|
||||
*/
|
||||
static void lockNumbering(final JdbcTemplate jdbcTemplate, final String series) {
|
||||
jdbcTemplate.queryForObject("SELECT 1 FROM pg_advisory_xact_lock(hashtext(?))", Integer.class, series);
|
||||
}
|
||||
|
||||
static String nextCertNo(final JdbcTemplate jdbcTemplate) {
|
||||
lockNumbering(jdbcTemplate, "m_investment_purchase.cert_no");
|
||||
final Integer max = jdbcTemplate.queryForObject(
|
||||
"SELECT MAX(CAST(cert_no AS INTEGER)) FROM m_investment_purchase WHERE cert_no ~ '^[0-9]+$'", Integer.class);
|
||||
return String.valueOf((max == null ? 0 : max) + 1);
|
||||
|
||||
+14
-7
@@ -106,11 +106,13 @@ public class InvestmentSaleQuoter {
|
||||
}
|
||||
final BigDecimal available = availableUnits(p, ignoreSaleId);
|
||||
if (r.units().compareTo(available) > 0) {
|
||||
throw rule("units.exceed", "Only " + available.stripTrailingZeros().toPlainString() + " units of deal " + p.getDealNo()
|
||||
+ " are available to sell");
|
||||
final boolean shares = InvestmentApiConstants.SHARE.equals(p.getBondType());
|
||||
throw rule("units.exceed", "Only " + available.stripTrailingZeros().toPlainString() + (shares ? " shares" : " units")
|
||||
+ " of deal " + p.getDealNo() + (shares ? " are left to refund" : " are available to sell"));
|
||||
}
|
||||
|
||||
final boolean mutualFund = "MUTUAL_FUND".equals(p.getBondType());
|
||||
final boolean share = InvestmentApiConstants.SHARE.equals(p.getBondType());
|
||||
// Non-SLR (bond, mutual fund) has no interest scheme (Jitendra Sir): the whole gain is the price difference.
|
||||
final boolean nonSlr = InvestmentApiConstants.NON_SLR_BOND_TYPES.contains(p.getBondType());
|
||||
final BigDecimal unitValue = p.getScript().getFaceValuePerUnit();
|
||||
@@ -121,6 +123,10 @@ public class InvestmentSaleQuoter {
|
||||
consideration = r.units().multiply(r.saleRate()).setScale(2, RoundingMode.HALF_UP);
|
||||
face = p.getUnits().signum() == 0 ? BigDecimal.ZERO
|
||||
: p.getFaceValue().multiply(r.units()).divide(p.getUnits(), 2, RoundingMode.HALF_UP);
|
||||
} else if (share) {
|
||||
// Share capital is refunded (retired) at a price per share, usually its face value.
|
||||
face = r.units().multiply(unitValue).setScale(2, RoundingMode.HALF_UP);
|
||||
consideration = r.units().multiply(r.saleRate()).setScale(2, RoundingMode.HALF_UP);
|
||||
} else {
|
||||
face = r.units().multiply(unitValue).setScale(2, RoundingMode.HALF_UP);
|
||||
consideration = face.multiply(r.saleRate()).divide(HUNDRED, 2, RoundingMode.HALF_UP);
|
||||
@@ -128,7 +134,7 @@ public class InvestmentSaleQuoter {
|
||||
int days = 0;
|
||||
BigDecimal accrued = BigDecimal.ZERO;
|
||||
BigDecimal receivable = BigDecimal.ZERO;
|
||||
if (!nonSlr) {
|
||||
if (!nonSlr && !share) {
|
||||
final LocalDate lastCoupon = lastCoupon(p, settlementDate);
|
||||
days = InvestmentInterestCalculator.days360(lastCoupon, settlementDate);
|
||||
final BigDecimal rate = p.getIntRate() == null ? BigDecimal.ZERO : p.getIntRate();
|
||||
@@ -174,8 +180,8 @@ public class InvestmentSaleQuoter {
|
||||
public InvestmentSale quoteRedemption(final InvestmentPurchase p, final Long bankGlAccountId, final String remarks) {
|
||||
requireActiveDebtHolding(p);
|
||||
if (p.getMaturityDate() == null) {
|
||||
throw rule("redeem.no.maturity", "Deal " + p.getDealNo() + " has no maturity date; redeem a mutual fund at its NAV through the "
|
||||
+ "Sell / Redeem entry");
|
||||
throw rule("redeem.no.maturity", "Deal " + p.getDealNo() + " has no maturity date; redeem a mutual fund at its NAV, or a "
|
||||
+ "share refund, through the Sell / Redeem entry");
|
||||
}
|
||||
if (p.getMaturityDate().isAfter(DateUtils.getBusinessLocalDate())) {
|
||||
throw rule("redeem.before.maturity", "Deal " + p.getDealNo() + " matures on " + p.getMaturityDate().format(DATE)
|
||||
@@ -299,8 +305,9 @@ public class InvestmentSaleQuoter {
|
||||
if (!STATUS_ACTIVE.equals(p.getStatus())) {
|
||||
throw rule("not.active", "Deal " + p.getDealNo() + " is not an active (settled) holding");
|
||||
}
|
||||
if (!InvestmentApiConstants.DEBT_BOND_TYPES.contains(p.getBondType()) && !"MUTUAL_FUND".equals(p.getBondType())) {
|
||||
throw rule("type.unsupported", "Sale and redemption are available for Govt Security, Bond and Mutual Fund holdings");
|
||||
if (!InvestmentApiConstants.DEBT_BOND_TYPES.contains(p.getBondType()) && !"MUTUAL_FUND".equals(p.getBondType())
|
||||
&& !InvestmentApiConstants.SHARE.equals(p.getBondType())) {
|
||||
throw rule("type.unsupported", "Sale and redemption are available for Govt Security, Bond, Mutual Fund and Share holdings");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -205,7 +205,9 @@ public class InvestmentSaleWritePlatformServiceImpl implements InvestmentSaleWri
|
||||
// more would leave the investment GL short when the deal is later sold or redeemed.
|
||||
final BigDecimal limit = p.getPremiumGlAccount() != null ? this.quoter.remainingBook(p).subtract(this.quoter.unamortisedPremium(p))
|
||||
: this.quoter.remainingBook(p);
|
||||
if (amount.compareTo(limit) >= 0) {
|
||||
// Shares of an institution in liquidation may be written off in full: they stay held at nil until the liquidator pays.
|
||||
final boolean liquidatedShare = InvestmentApiConstants.SHARE.equals(p.getBondType()) && p.getParty().isInLiquidation();
|
||||
if (liquidatedShare ? amount.compareTo(limit) > 0 : amount.compareTo(limit) >= 0) {
|
||||
throw InvestmentSaleQuoter.rule("writeoff.exceeds", "The write-off must be less than "
|
||||
+ limit.stripTrailingZeros().toPlainString() + " (book value less premium still to be amortised); "
|
||||
+ "sell or redeem the security to close it");
|
||||
|
||||
+290
@@ -0,0 +1,290 @@
|
||||
/**
|
||||
* 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 static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.INSTITUTION_OTHER_COOP_SOCIETY;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SHARE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.AMOUNT;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.BANK_GL_ACCOUNT_ID;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.DIVIDEND_DATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.DIVIDEND_GL_CODE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.DIVIDEND_PARAMS;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.REMARKS;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.STATUS_ACTIVE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.STATUS_PENDING_SETTLEMENT;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.STATUS_SOLD;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import java.lang.reflect.Type;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
|
||||
import org.apache.fineract.accounting.glaccount.domain.GLAccountRepositoryWrapper;
|
||||
import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants;
|
||||
import org.apache.fineract.portfolio.investment.data.InvestmentDividendData;
|
||||
import org.apache.fineract.portfolio.investment.data.InvestmentShareLimitData;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentDepositBase;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentDepositBaseRepository;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentDividend;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentDividendRepository;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentParty;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentPartyRepository;
|
||||
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.apache.fineract.portfolio.investment.exception.InvestmentPartyNotFoundException;
|
||||
import org.apache.fineract.portfolio.investment.exception.InvestmentPurchaseNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* Share investments a UCB may hold: shares of its DCCB / State Co-op Bank, of co-operative societies in its area of
|
||||
* operation, and membership shares of NPCI / CCIL / SWIFT, the Umbrella Organisation and IDPIC. Shares of co-operative
|
||||
* societies are limited to 2% of the bank's owned funds in all and to 5% of one society's subscribed capital; membership
|
||||
* shares may exceed these. Dividends are income when received.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class InvestmentShareService {
|
||||
|
||||
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
||||
private static final BigDecimal OWNED_FUNDS_PERCENT = BigDecimal.valueOf(2);
|
||||
private static final BigDecimal SUBSCRIBED_CAPITAL_PERCENT = BigDecimal.valueOf(5);
|
||||
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
private final InvestmentPurchaseRepository purchaseRepository;
|
||||
private final InvestmentSaleRepository saleRepository;
|
||||
private final InvestmentPartyRepository partyRepository;
|
||||
private final InvestmentDepositBaseRepository depositBaseRepository;
|
||||
private final InvestmentDividendRepository dividendRepository;
|
||||
private final GLAccountRepositoryWrapper glAccountRepository;
|
||||
private final InvestmentJournalPoster journalPoster;
|
||||
|
||||
/** Shares of a co-operative society: the holdings the 2% and 5% limits (and the 10% non-SLR limit) apply to. */
|
||||
static boolean societyShare(final InvestmentPurchase p) {
|
||||
return SHARE.equals(p.getBondType()) && p.getParty() != null
|
||||
&& INSTITUTION_OTHER_COOP_SOCIETY.equals(p.getParty().getInstitutionType());
|
||||
}
|
||||
|
||||
/** The RBI limits for a share purchase of {@code amount} from {@code partyId} (either may be empty) on {@code dealDate}. */
|
||||
@Transactional(readOnly = true)
|
||||
public InvestmentShareLimitData limits(final LocalDate dealDate, final Long partyId, final BigDecimal amount) {
|
||||
final InvestmentParty party = partyId == null ? null
|
||||
: this.partyRepository.findById(partyId).orElseThrow(() -> new InvestmentPartyNotFoundException(partyId));
|
||||
return limits(dealDate, party, amount);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public InvestmentShareLimitData limits(final LocalDate dealDate, final InvestmentParty party, final BigDecimal amount) {
|
||||
final LocalDate base = InvestmentFdService.previousYearEnd(dealDate);
|
||||
final BigDecimal ownedFunds = this.depositBaseRepository.findByAsOnDate(base).map(InvestmentDepositBase::getOwnedFunds)
|
||||
.orElse(null);
|
||||
final BigDecimal newAmount = amount == null ? BigDecimal.ZERO : amount;
|
||||
final boolean society = party != null && INSTITUTION_OTHER_COOP_SOCIETY.equals(party.getInstitutionType());
|
||||
|
||||
BigDecimal coopOutstanding = BigDecimal.ZERO;
|
||||
BigDecimal withParty = BigDecimal.ZERO;
|
||||
for (final InvestmentPurchase p : this.purchaseRepository.findAll()) {
|
||||
if (!societyShare(p)) {
|
||||
continue;
|
||||
}
|
||||
final BigDecimal now = bookNow(p);
|
||||
coopOutstanding = coopOutstanding.add(now);
|
||||
if (party != null && p.getParty().getId().equals(party.getId())) {
|
||||
withParty = withParty.add(now);
|
||||
}
|
||||
}
|
||||
final BigDecimal coopAfter = society ? coopOutstanding.add(newAmount) : coopOutstanding;
|
||||
final BigDecimal institutionAfter = withParty.add(newAmount);
|
||||
final BigDecimal capital = party == null ? null : party.getSubscribedCapital();
|
||||
final InvestmentShareLimitData.InvestmentShareLimitDataBuilder b = InvestmentShareLimitData.builder().asOnDate(base.format(DATE))
|
||||
.institutionType(party == null ? null : party.getInstitutionType()).exempt(party != null && !society)
|
||||
.ownedFunds(ownedFunds).coopLimit(ownedFunds == null ? null : percent(ownedFunds, OWNED_FUNDS_PERCENT))
|
||||
.coopOutstanding(coopOutstanding).coopAfter(coopAfter).partyName(party == null ? null : party.getPartyName())
|
||||
.subscribedCapital(capital).institutionLimit(capital == null ? null : percent(capital, SUBSCRIBED_CAPITAL_PERCENT))
|
||||
.institutionOutstanding(withParty).institutionAfter(institutionAfter).newAmount(newAmount);
|
||||
if (party == null) {
|
||||
return b.withinLimits(true).message("Choose the institution").build();
|
||||
}
|
||||
if (!society) {
|
||||
return b.withinLimits(true).message("Membership shares of a " + label(party.getInstitutionType())
|
||||
+ " may exceed the RBI limits on shares of co-operative societies").build();
|
||||
}
|
||||
if (ownedFunds == null) {
|
||||
return b.withinLimits(false).message("Enter the bank's owned funds (paid-up share capital + reserves) as on "
|
||||
+ base.format(DATE) + " with the deposit base first; shares of co-operative societies are limited to 2% of it")
|
||||
.build();
|
||||
}
|
||||
final BigDecimal coopLimit = percent(ownedFunds, OWNED_FUNDS_PERCENT);
|
||||
if (coopAfter.compareTo(coopLimit) > 0) {
|
||||
return b.withinLimits(false).message("Shares of co-operative societies would be " + InvestmentNonSlrService.plain(coopAfter)
|
||||
+ ", above the RBI limit of " + InvestmentNonSlrService.plain(coopLimit) + " (2% of owned funds "
|
||||
+ InvestmentNonSlrService.plain(ownedFunds) + " as on " + base.format(DATE) + ")").build();
|
||||
}
|
||||
if (capital == null) {
|
||||
return b.withinLimits(false).message("Enter the subscribed share capital of " + party.getPartyName()
|
||||
+ " in Bank / Party Master first; the bank may hold at most 5% of it").build();
|
||||
}
|
||||
final BigDecimal institutionLimit = percent(capital, SUBSCRIBED_CAPITAL_PERCENT);
|
||||
if (institutionAfter.compareTo(institutionLimit) > 0) {
|
||||
return b.withinLimits(false).message("Shares of " + party.getPartyName() + " would be "
|
||||
+ InvestmentNonSlrService.plain(institutionAfter) + ", above the RBI limit of " + InvestmentNonSlrService.plain(institutionLimit)
|
||||
+ " (5% of its subscribed capital " + InvestmentNonSlrService.plain(capital) + ")").build();
|
||||
}
|
||||
return b.withinLimits(true).message("Within the RBI limits").build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a dividend received on a share holding: Dr the bank GL / Cr 400404 Dividend on Investments, dated the day it
|
||||
* was received, with a voucher. Income is taken when it is received; nothing is accrued.
|
||||
*/
|
||||
@Transactional
|
||||
public CommandProcessingResult postDividend(final Long purchaseId, final JsonCommand command) {
|
||||
validateDividend(command.json());
|
||||
final InvestmentPurchase p = this.purchaseRepository.findByIdWithLock(purchaseId)
|
||||
.orElseThrow(() -> new InvestmentPurchaseNotFoundException(purchaseId));
|
||||
if (!SHARE.equals(p.getBondType())) {
|
||||
throw rule("not.share", "Deal " + p.getDealNo() + " is not a share holding; a dividend is entered on shares only");
|
||||
}
|
||||
// A dividend for a year the shares were held can come in after they were refunded.
|
||||
if (!STATUS_ACTIVE.equals(p.getStatus()) && !STATUS_SOLD.equals(p.getStatus())) {
|
||||
throw rule("not.held", "Deal " + p.getDealNo() + " is not a settled holding");
|
||||
}
|
||||
final LocalDate date = command.localDateValueOfParameterNamed(DIVIDEND_DATE);
|
||||
if (date.isAfter(DateUtils.getBusinessLocalDate())) {
|
||||
throw rule("date.future", "The dividend date cannot be after today");
|
||||
}
|
||||
if (date.isBefore(p.getSettlementDate())) {
|
||||
throw rule("date.before.purchase", "The dividend date cannot be before the shares were bought (settled "
|
||||
+ p.getSettlementDate().format(DATE) + ")");
|
||||
}
|
||||
final BigDecimal amount = command.bigDecimalValueOfParameterNamed(AMOUNT).setScale(2, RoundingMode.HALF_UP);
|
||||
final GLAccount bank = command.parameterExists(BANK_GL_ACCOUNT_ID) && command.longValueOfParameterNamed(BANK_GL_ACCOUNT_ID) != null
|
||||
? postableAssetGl(command.longValueOfParameterNamed(BANK_GL_ACCOUNT_ID))
|
||||
: p.getCrGlAccount();
|
||||
|
||||
final CommandProcessingResult posted = this.journalPoster.post(date, p.getDealNo(),
|
||||
"Dividend received: deal " + p.getDealNo() + ", " + p.getScript().getScriptCode() + " " + p.getScript().getScriptDesc()
|
||||
+ ", " + p.getParty().getPartyName(),
|
||||
new InvestmentJournalPoster.Lines().debit(bank.getId(), amount).credit(this.journalPoster.glId(DIVIDEND_GL_CODE), amount));
|
||||
final Object voucherNo = posted.getChanges() == null ? null : posted.getChanges().get("voucherNo");
|
||||
|
||||
final InvestmentDividend d = new InvestmentDividend();
|
||||
d.setPurchase(p);
|
||||
d.setDividendDate(date);
|
||||
d.setAmount(amount);
|
||||
d.setBankGlAccount(bank);
|
||||
d.setRemarks(StringUtils.trimToNull(command.stringValueOfParameterNamed(REMARKS)));
|
||||
d.setGlTransactionId(posted.getTransactionId());
|
||||
d.setVoucherNo(voucherNo == null ? null : voucherNo.toString());
|
||||
final InvestmentDividend saved = this.dividendRepository.saveAndFlush(d);
|
||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(saved.getId()).withSubEntityId(purchaseId)
|
||||
.withResourceIdAsString(saved.getVoucherNo()).withTransactionId(posted.getTransactionId()).build();
|
||||
}
|
||||
|
||||
/** Dividends received, newest first. */
|
||||
@Transactional(readOnly = true)
|
||||
public List<InvestmentDividendData> retrieveDividends() {
|
||||
return this.dividendRepository.findAllByOrderByDividendDateDescIdDesc().stream().map(InvestmentDividendData::from).toList();
|
||||
}
|
||||
|
||||
/** Book value of a share deal now: cost less settled refunds and write-offs; a pending deal counts at its full cost. */
|
||||
private BigDecimal bookNow(final InvestmentPurchase p) {
|
||||
if (STATUS_PENDING_SETTLEMENT.equals(p.getStatus())) {
|
||||
return p.getBookValue();
|
||||
}
|
||||
if (!STATUS_ACTIVE.equals(p.getStatus())) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
BigDecimal book = p.getBookValue();
|
||||
for (final InvestmentSale s : this.saleRepository.findByPurchaseIdOrderByIdAsc(p.getId())) {
|
||||
if (InvestmentSaleApiConstants.STATUS_SETTLED.equals(s.getStatus()) && s.getBookValueSold() != null) {
|
||||
book = book.subtract(s.getBookValueSold());
|
||||
}
|
||||
}
|
||||
return book;
|
||||
}
|
||||
|
||||
private static String label(final String institutionType) {
|
||||
return switch (institutionType == null ? "" : institutionType) {
|
||||
case "STATE_COOP_BANK" -> "State Co-op Bank";
|
||||
case "DCCB" -> "District Central Co-op Bank";
|
||||
case "MEMBERSHIP_MIC_UO" -> "market infrastructure company / Umbrella Organisation";
|
||||
default -> "co-operative institution";
|
||||
};
|
||||
}
|
||||
|
||||
private static BigDecimal percent(final BigDecimal base, final BigDecimal percent) {
|
||||
return base.multiply(percent).divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
/** The bank the dividend comes into must be an enabled ASSET detail GL. */
|
||||
private GLAccount postableAssetGl(final Long glAccountId) {
|
||||
final GLAccount gl = this.glAccountRepository.findOneWithNotFoundDetection(glAccountId);
|
||||
if (gl.isHeaderAccount() || gl.isDisabled() || !GLAccountType.ASSET.getValue().equals(gl.getType())) {
|
||||
throw rule("bank.glaccount.invalid", "Bank GL " + gl.getGlCode() + " must be an enabled Asset detail account");
|
||||
}
|
||||
return gl;
|
||||
}
|
||||
|
||||
private void validateDividend(final String json) {
|
||||
if (StringUtils.isBlank(json)) {
|
||||
throw new InvalidJsonException();
|
||||
}
|
||||
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
||||
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, DIVIDEND_PARAMS);
|
||||
final JsonElement element = this.fromApiJsonHelper.parse(json);
|
||||
final List<ApiParameterError> errors = new ArrayList<>();
|
||||
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource("investmentdividend");
|
||||
v.reset().parameter(DIVIDEND_DATE).value(this.fromApiJsonHelper.extractLocalDateNamed(DIVIDEND_DATE, element)).notNull();
|
||||
v.reset().parameter(AMOUNT).value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(AMOUNT, element)).notNull()
|
||||
.positiveAmount();
|
||||
v.reset().parameter(BANK_GL_ACCOUNT_ID).value(this.fromApiJsonHelper.extractLongNamed(BANK_GL_ACCOUNT_ID, element)).ignoreIfNull()
|
||||
.longGreaterThanZero();
|
||||
v.reset().parameter(REMARKS).value(this.fromApiJsonHelper.extractStringNamed(REMARKS, element)).ignoreIfNull()
|
||||
.notExceedingLengthOf(500);
|
||||
if (!errors.isEmpty()) {
|
||||
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors);
|
||||
}
|
||||
}
|
||||
|
||||
private static GeneralPlatformDomainRuleException rule(final String code, final String message) {
|
||||
return new GeneralPlatformDomainRuleException("error.msg.investmentshare." + code, message);
|
||||
}
|
||||
}
|
||||
+64
-5
@@ -23,6 +23,7 @@ import static org.apache.fineract.portfolio.investment.api.InvestmentValuationAp
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentValuationApiConstants.CLASS_BOND;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentValuationApiConstants.CLASS_FUND;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentValuationApiConstants.CLASS_GOVT;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentValuationApiConstants.CLASS_SHARE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentValuationApiConstants.DEPRECIATION_PROVISION_GL_CODE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentValuationApiConstants.DEPRECIATION_RESERVE_GL_CODE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentValuationApiConstants.RUN_PARAMS;
|
||||
@@ -36,10 +37,12 @@ import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
@@ -52,11 +55,14 @@ import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
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.InvestmentValuationData;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentDailyRate;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentDailyRateRepository;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentDividendRepository;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentParty;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchase;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchaseRepository;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentSale;
|
||||
@@ -77,8 +83,9 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
* Market valuation (MTM) of AFS and HFT holdings (RBI: HFT monthly, AFS quarterly, or more often; HTM is carried at cost).
|
||||
* Each security still held on the valuation date is valued at the latest Daily Script Master rate on or before that date
|
||||
* — a bond or Govt security at units x unit value x rate / 100, a mutual fund at units x NAV — against the book value
|
||||
* still held. Gains and losses are netted within each class (Govt securities, corporate bonds, mutual funds); a net loss
|
||||
* is the IDR the class needs, a net gain is ignored, and one class never offsets another. The book value does not
|
||||
* still held. Shares have no market rate and are valued by rule (see {@link #shareLine}). Gains and losses are netted
|
||||
* within each class (Govt securities, corporate bonds, mutual funds, shares); a net loss is the IDR the class needs, a net
|
||||
* gain is ignored, and one class never offsets another. The book value does not
|
||||
* change: the run posts only the difference from the IDR the category's previous run held — Dr 500504 Provision for
|
||||
* Depreciation / Cr 200205 Investment Depreciation Reserve when it grows, the reverse (a write-back) when it falls.
|
||||
*/
|
||||
@@ -88,13 +95,14 @@ public class InvestmentValuationService {
|
||||
|
||||
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
||||
private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);
|
||||
private static final List<String> VALUED_BOND_TYPES = List.of("GOVT_SECURITY", "NON_SLR", "BOND", "MUTUAL_FUND");
|
||||
private static final List<String> VALUED_BOND_TYPES = List.of("GOVT_SECURITY", "NON_SLR", "BOND", "MUTUAL_FUND", "SHARE");
|
||||
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
private final InvestmentPurchaseRepository purchaseRepository;
|
||||
private final InvestmentSaleRepository saleRepository;
|
||||
private final InvestmentSecurityTransferRepository transferRepository;
|
||||
private final InvestmentDailyRateRepository dailyRateRepository;
|
||||
private final InvestmentDividendRepository dividendRepository;
|
||||
private final InvestmentValuationRepository valuationRepository;
|
||||
private final InvestmentValuationLineRepository lineRepository;
|
||||
private final InvestmentJournalPoster journalPoster;
|
||||
@@ -156,6 +164,7 @@ public class InvestmentValuationService {
|
||||
v.setGovtRequired(required.get(CLASS_GOVT));
|
||||
v.setBondRequired(required.get(CLASS_BOND));
|
||||
v.setFundRequired(required.get(CLASS_FUND));
|
||||
v.setShareRequired(required.get(CLASS_SHARE));
|
||||
v.setRequiredTotal(total);
|
||||
v.setPreviousTotal(previous);
|
||||
v.setDelta(delta);
|
||||
@@ -208,6 +217,7 @@ public class InvestmentValuationService {
|
||||
private Valued value(final LocalDate valuationDate, final String category) {
|
||||
final List<InvestmentValuationLine> lines = new ArrayList<>();
|
||||
final List<String> missing = new ArrayList<>();
|
||||
final Set<Long> rupeeTaken = new HashSet<>();
|
||||
for (final InvestmentPurchase p : this.purchaseRepository.findAll(Sort.by(Sort.Direction.ASC, "id"))) {
|
||||
if (!VALUED_BOND_TYPES.contains(p.getBondType()) || p.getCategory() == null || !p.getCategory().startsWith(category)
|
||||
|| InvestmentPurchaseApiConstants.STATUS_DROPPED.equals(p.getStatus())
|
||||
@@ -230,6 +240,10 @@ public class InvestmentValuationService {
|
||||
if (units.signum() <= 0) {
|
||||
continue;
|
||||
}
|
||||
if (InvestmentApiConstants.SHARE.equals(p.getBondType())) {
|
||||
lines.add(shareLine(p, units, book.setScale(2, RoundingMode.HALF_UP), valuationDate, rupeeTaken));
|
||||
continue;
|
||||
}
|
||||
// A holding transferred after the valuation date was still the old security on that date.
|
||||
final InvestmentScript script = this.transferRepository.findByPurchaseIdOrderByTransferDateAscIdAsc(p.getId()).stream()
|
||||
.filter(t -> t.getTransferDate().isAfter(valuationDate)).findFirst().map(InvestmentSecurityTransfer::getFromScript)
|
||||
@@ -276,10 +290,53 @@ public class InvestmentValuationService {
|
||||
return "MUTUAL_FUND".equals(bondType) ? CLASS_FUND : CLASS_BOND;
|
||||
}
|
||||
|
||||
/**
|
||||
* A share is valued by rule, not at a market rate. Shares of a co-operative institution (DCCB, State Co-op Bank,
|
||||
* society) are held at face value while it pays us dividends; at nil (fully provided) when it is in liquidation or has
|
||||
* paid us no dividend; at ₹1 for the institution, and as a non-performing investment, when its latest balance sheet
|
||||
* is over 18 months old. Membership shares of NPCI / CCIL / SWIFT / the Umbrella Organisation stay at face value.
|
||||
*/
|
||||
private InvestmentValuationLine shareLine(final InvestmentPurchase p, final BigDecimal units, final BigDecimal book,
|
||||
final LocalDate valuationDate, final Set<Long> rupeeTaken) {
|
||||
final InvestmentParty party = p.getParty();
|
||||
BigDecimal market = units.multiply(p.getScript().getFaceValuePerUnit()).setScale(2, RoundingMode.HALF_UP);
|
||||
String basis = "Face value";
|
||||
boolean npi = false;
|
||||
if (InvestmentApiConstants.COOP_INSTITUTION_TYPES.contains(party.getInstitutionType())) {
|
||||
npi = party.getLastBalanceSheetDate() == null || party.getLastBalanceSheetDate().isBefore(valuationDate.minusMonths(18));
|
||||
if (party.isInLiquidation()) {
|
||||
market = BigDecimal.ZERO.setScale(2);
|
||||
basis = "In liquidation - fully provided";
|
||||
} else if (!this.dividendRepository.existsByPurchasePartyIdAndDividendDateLessThanEqual(party.getId(), valuationDate)) {
|
||||
market = BigDecimal.ZERO.setScale(2);
|
||||
basis = "No dividend received - fully provided";
|
||||
} else if (npi) {
|
||||
// Rs 1 for the institution as a whole, not for each of our holdings of its shares.
|
||||
market = (rupeeTaken.add(party.getId()) ? BigDecimal.ONE : BigDecimal.ZERO).setScale(2);
|
||||
basis = "Balance sheet over 18 months old - Rs 1";
|
||||
} else {
|
||||
basis = "Face value (dividend received)";
|
||||
}
|
||||
}
|
||||
final InvestmentValuationLine line = new InvestmentValuationLine();
|
||||
line.setPurchase(p);
|
||||
line.setScript(p.getScript());
|
||||
line.setAssetClass(CLASS_SHARE);
|
||||
line.setUnits(units);
|
||||
line.setBookValue(book);
|
||||
line.setRate(units.signum() == 0 ? BigDecimal.ZERO : market.divide(units, 6, RoundingMode.HALF_UP));
|
||||
line.setRateDate(valuationDate);
|
||||
line.setMarketValue(market);
|
||||
line.setDifference(market.subtract(book));
|
||||
line.setValuationBasis(basis);
|
||||
line.setNpi(npi);
|
||||
return line;
|
||||
}
|
||||
|
||||
/** IDR each class needs: its net loss; a net gain needs none. */
|
||||
private static Map<String, BigDecimal> required(final List<InvestmentValuationLine> lines) {
|
||||
final Map<String, BigDecimal> required = new LinkedHashMap<>();
|
||||
for (final String c : List.of(CLASS_GOVT, CLASS_BOND, CLASS_FUND)) {
|
||||
for (final String c : List.of(CLASS_GOVT, CLASS_BOND, CLASS_FUND, CLASS_SHARE)) {
|
||||
final BigDecimal net = lines.stream().filter(l -> c.equals(l.getAssetClass())).map(InvestmentValuationLine::getDifference)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
required.put(c, net.signum() < 0 ? net.negate() : BigDecimal.ZERO);
|
||||
@@ -309,6 +366,7 @@ public class InvestmentValuationService {
|
||||
required.put(CLASS_GOVT, v.getGovtRequired());
|
||||
required.put(CLASS_BOND, v.getBondRequired());
|
||||
required.put(CLASS_FUND, v.getFundRequired());
|
||||
required.put(CLASS_SHARE, v.getShareRequired());
|
||||
return InvestmentValuationData.builder().id(v.getId()).valuationDate(v.getValuationDate().format(DATE)).category(v.getCategory())
|
||||
.requiredTotal(v.getRequiredTotal()).previousTotal(v.getPreviousTotal()).delta(v.getDelta())
|
||||
.provisionGlCode(DEPRECIATION_PROVISION_GL_CODE).reserveGlCode(DEPRECIATION_RESERVE_GL_CODE)
|
||||
@@ -321,7 +379,8 @@ public class InvestmentValuationService {
|
||||
return InvestmentValuationData.Line.builder().purchaseId(p.getId()).dealNo(p.getDealNo()).certNo(p.getCertNo())
|
||||
.bondType(p.getBondType()).scriptCode(l.getScript().getScriptCode()).scriptDesc(l.getScript().getScriptDesc())
|
||||
.assetClass(l.getAssetClass()).units(l.getUnits()).bookValue(l.getBookValue()).rate(l.getRate())
|
||||
.rateDate(l.getRateDate().format(DATE)).marketValue(l.getMarketValue()).difference(l.getDifference()).build();
|
||||
.rateDate(l.getRateDate().format(DATE)).marketValue(l.getMarketValue()).difference(l.getDifference())
|
||||
.valuationBasis(l.getValuationBasis()).npi(l.isNpi()).build();
|
||||
}
|
||||
|
||||
private void validate(final String json) {
|
||||
|
||||
@@ -203,4 +203,5 @@
|
||||
<include file="parts/0609_investment_script_rating.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0610_investment_valuation_and_fund_switch.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0611_investment_maker_checker.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0612_investment_share.xml" relativeToChangelogFile="true"/>
|
||||
</databaseChangeLog>
|
||||
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
|
||||
<!--
|
||||
|
||||
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.
|
||||
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.9.xsd">
|
||||
<!--
|
||||
Share investments a UCB may hold: shares of its DCCB / State Co-op Bank, of co-operative societies in its area of
|
||||
operation, and membership shares of market infrastructure companies (NPCI, CCIL, SWIFT), the Umbrella Organisation
|
||||
and IDPIC. The institution (party) carries its type, its subscribed capital (for the 5% per-institution limit), the
|
||||
date of its latest balance sheet and whether it is in liquidation (both decide its valuation). The bank's owned
|
||||
funds as on 31 March sit next to the deposit base (for the 2% limit on co-operative shares). Dividends received are
|
||||
recorded with their voucher; a valuation keeps a separate class for shares.
|
||||
-->
|
||||
<changeSet author="cbs-anshul" id="0612-1-investment-party-share-fields">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not><columnExists tableName="m_investment_party" columnName="institution_type"/></not>
|
||||
</preConditions>
|
||||
<addColumn tableName="m_investment_party">
|
||||
<column name="institution_type" type="VARCHAR(30)"/>
|
||||
<column name="subscribed_capital" type="DECIMAL(19,6)"/>
|
||||
<column name="last_balance_sheet_date" type="DATE"/>
|
||||
<column name="is_in_liquidation" type="BOOLEAN" defaultValueBoolean="false"><constraints nullable="false"/></column>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
<changeSet author="cbs-anshul" id="0612-2-investment-deposit-base-owned-funds">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not><columnExists tableName="m_investment_deposit_base" columnName="owned_funds"/></not>
|
||||
</preConditions>
|
||||
<addColumn tableName="m_investment_deposit_base">
|
||||
<column name="owned_funds" type="DECIMAL(19,6)"/>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
<changeSet author="cbs-anshul" id="0612-3-create-investment-dividend">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not><tableExists tableName="m_investment_dividend"/></not>
|
||||
</preConditions>
|
||||
<createTable tableName="m_investment_dividend">
|
||||
<column name="id" type="BIGINT" autoIncrement="true"><constraints nullable="false" primaryKey="true"/></column>
|
||||
<column name="purchase_id" type="BIGINT"><constraints nullable="false"/></column>
|
||||
<column name="dividend_date" type="DATE"><constraints nullable="false"/></column>
|
||||
<column name="amount" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
|
||||
<column name="bank_gl_account_id" type="BIGINT"><constraints nullable="false"/></column>
|
||||
<column name="remarks" type="VARCHAR(500)"/>
|
||||
<column name="gl_transaction_id" type="VARCHAR(50)"/>
|
||||
<column name="voucher_no" type="VARCHAR(30)"/>
|
||||
<column name="created_by" type="BIGINT"><constraints nullable="false"/></column>
|
||||
<column name="created_on_utc" type="DATETIME"><constraints nullable="false"/></column>
|
||||
<column name="last_modified_by" type="BIGINT"><constraints nullable="false"/></column>
|
||||
<column name="last_modified_on_utc" type="DATETIME"><constraints nullable="false"/></column>
|
||||
</createTable>
|
||||
<addForeignKeyConstraint baseTableName="m_investment_dividend" baseColumnNames="purchase_id" constraintName="fk_inv_dividend_purchase"
|
||||
referencedTableName="m_investment_purchase" referencedColumnNames="id"/>
|
||||
<addForeignKeyConstraint baseTableName="m_investment_dividend" baseColumnNames="bank_gl_account_id" constraintName="fk_inv_dividend_bank_gl"
|
||||
referencedTableName="acc_gl_account" referencedColumnNames="id"/>
|
||||
</changeSet>
|
||||
<changeSet author="cbs-anshul" id="0612-4-investment-valuation-share-class">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not><columnExists tableName="m_investment_valuation" columnName="share_required"/></not>
|
||||
</preConditions>
|
||||
<addColumn tableName="m_investment_valuation">
|
||||
<column name="share_required" type="DECIMAL(19,6)" defaultValueNumeric="0"><constraints nullable="false"/></column>
|
||||
</addColumn>
|
||||
<addColumn tableName="m_investment_valuation_line">
|
||||
<column name="valuation_basis" type="VARCHAR(100)"/>
|
||||
<column name="is_npi" type="BOOLEAN" defaultValueBoolean="false"><constraints nullable="false"/></column>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
<!-- A dividend voucher goes to the Checker Inbox like every other investment entry. -->
|
||||
<changeSet author="cbs-anshul" id="0612-5-investment-dividend-permissions">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'DIVIDEND_INVESTMENTPURCHASE'</sqlCheck>
|
||||
</preConditions>
|
||||
<sql>
|
||||
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) VALUES
|
||||
('portfolio', 'DIVIDEND_INVESTMENTPURCHASE', 'INVESTMENTPURCHASE', 'DIVIDEND', TRUE),
|
||||
('portfolio', 'DIVIDEND_INVESTMENTPURCHASE_CHECKER', 'INVESTMENTPURCHASE', 'DIVIDEND_CHECKER', FALSE)
|
||||
</sql>
|
||||
</changeSet>
|
||||
<changeSet author="cbs-anshul" id="0612-6-grant-investment-dividend-permission">
|
||||
<sql>
|
||||
INSERT INTO m_role_permission (role_id, permission_id)
|
||||
SELECT rp.role_id, p.id
|
||||
FROM m_role_permission rp
|
||||
JOIN m_permission menu ON menu.id = rp.permission_id AND menu.code = 'MENU_INVESTMENT'
|
||||
CROSS JOIN m_permission p
|
||||
WHERE p.code = 'DIVIDEND_INVESTMENTPURCHASE'
|
||||
AND NOT EXISTS (SELECT 1 FROM m_role_permission x WHERE x.role_id = rp.role_id AND x.permission_id = p.id)
|
||||
</sql>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
Reference in New Issue
Block a user