feat(investment): FD with other banks - placement, maturity, premature, renewal, interest and RBI limits
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s

This commit is contained in:
2026-10-16 17:12:40 +05:30
parent ac563badaa
commit 4522f1305d
33 changed files with 2795 additions and 2 deletions
@@ -183,6 +183,54 @@ public class CommandWrapperBuilder {
return this;
}
public CommandWrapperBuilder createInvestmentFd() {
this.actionName = "CREATE";
this.entityName = "INVESTMENTFD";
this.entityId = null;
this.href = "/investment/fds";
return this;
}
public CommandWrapperBuilder matureInvestmentFd(final Long fdId) {
this.actionName = "MATURE";
this.entityName = "INVESTMENTFD";
this.entityId = fdId;
this.href = "/investment/fds/" + fdId + "/mature";
return this;
}
public CommandWrapperBuilder prematureInvestmentFd(final Long fdId) {
this.actionName = "PREMATURE";
this.entityName = "INVESTMENTFD";
this.entityId = fdId;
this.href = "/investment/fds/" + fdId + "/premature";
return this;
}
public CommandWrapperBuilder renewInvestmentFd(final Long fdId) {
this.actionName = "RENEW";
this.entityName = "INVESTMENTFD";
this.entityId = fdId;
this.href = "/investment/fds/" + fdId + "/renew";
return this;
}
public CommandWrapperBuilder postInvestmentFdInterest(final Long fdId) {
this.actionName = "POSTINTEREST";
this.entityName = "INVESTMENTFD";
this.entityId = fdId;
this.href = "/investment/fds/" + fdId + "/interest";
return this;
}
public CommandWrapperBuilder setInvestmentDepositBase() {
this.actionName = "SETBASE";
this.entityName = "INVESTMENTFD";
this.entityId = null;
this.href = "/investment/fds/deposit-base";
return this;
}
public CommandWrapperBuilder amortiseInvestmentPremium() {
this.actionName = "AMORTISE";
this.entityName = "INVESTMENTSALE";
@@ -46,6 +46,7 @@ import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.cashreport.service.VaultPositionService;
import org.apache.fineract.portfolio.denominationmaster.service.CountedCashLedger;
import org.apache.fineract.portfolio.investment.service.InvestmentFdInterestService;
import org.apache.fineract.portfolio.investment.service.InvestmentPurchaseReadPlatformService;
import org.apache.fineract.portfolio.savings.dailyinterest.SavingsDailyInterestOrchestrator;
import org.apache.fineract.portfolio.savings.dailyinterest.SavingsInterestPostingReportService;
@@ -97,6 +98,7 @@ public class BranchDayRegisterApiResource {
private final FixedDepositInterestPostingReportService fdInterestPostingReportService;
private final FixedDepositInterestPostingOrchestrator fdInterestPostingOrchestrator;
private final InvestmentPurchaseReadPlatformService investmentPurchaseReadPlatformService;
private final InvestmentFdInterestService investmentFdInterestService;
private final Gson gson = new Gson();
private boolean isHeadOffice(final Long officeId) {
@@ -273,6 +275,8 @@ public class BranchDayRegisterApiResource {
// only the Head Office close shows it.
result.put("investmentInterestPending",
isHeadOffice(officeId) ? investmentPurchaseReadPlatformService.retrieveDueInterest(0) : List.of());
// The same for FDs placed with other banks: interest vouchers and maturities due. A reminder only.
result.put("investmentFdPending", isHeadOffice(officeId) ? investmentFdInterestService.due(0) : List.of());
return gson.toJson(result);
}
@@ -52,6 +52,7 @@ public final class InvestmentApiConstants {
// Common parameters
public static final String BOND_TYPE = "bondType";
public static final String GL_ACCOUNT_ID = "glAccountId";
public static final String INTEREST_GL_ACCOUNT_ID = "interestGlAccountId";
public static final String ADDRESS1 = "address1";
public static final String ADDRESS2 = "address2";
public static final String ADDRESS3 = "address3";
@@ -86,7 +87,7 @@ public final class InvestmentApiConstants {
public static final String CORP_CODE = "corpCode";
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, RBI_CODE, EXPOSURE_LIMIT, LEI_NO, CP_BANK_NAME, CP_BRANCH_NAME, CP_ACCOUNT_NO, CP_IFSC, ACTIVE,
BANK_TYPE, GL_ACCOUNT_ID, INTEREST_GL_ACCOUNT_ID, RBI_CODE, EXPOSURE_LIMIT, LEI_NO, CP_BANK_NAME, CP_BRANCH_NAME, CP_ACCOUNT_NO, CP_IFSC, ACTIVE,
LOCALE);
public static final Set<String> SCRIPT_PARAMS = Set.of(BOND_TYPE, SLR_TYPE, SCRIPT_DESC, GL_ACCOUNT_ID, ISIN, FACE_VALUE_PER_UNIT,
@@ -0,0 +1,119 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.investment.api;
import java.math.BigDecimal;
import java.util.Set;
/** Fixed deposits placed with other banks. */
public final class InvestmentFdApiConstants {
private InvestmentFdApiConstants() {}
public static final String ENTITY = "INVESTMENTFD";
public static final String BOND_TYPE_FD = "FD";
public static final String STATUS_ACTIVE = "ACTIVE";
public static final String STATUS_MATURED = "MATURED";
public static final String STATUS_PREMATURE = "PREMATURE";
public static final String STATUS_RENEWED = "RENEWED";
/** When the interest is paid: all at maturity (the usual case), or every half year / year. */
public static final String PAYOUT_AT_MATURITY = "MATURITY";
public static final String PAYOUT_HALF_YEARLY = "HALF_YEARLY";
public static final String PAYOUT_YEARLY = "YEARLY";
public static final Set<String> PAYOUTS = Set.of(PAYOUT_AT_MATURITY, PAYOUT_HALF_YEARLY, PAYOUT_YEARLY);
/** How the maturity amount is worked out, to compare with the bank's figure (Jitendra Sir). */
public static final String CALC_SIMPLE = "SIMPLE";
public static final String CALC_MONTHLY = "MONTHLY";
public static final String CALC_QUARTERLY = "QUARTERLY";
public static final String CALC_HALF_YEARLY = "HALF_YEARLY";
public static final String CALC_YEARLY = "YEARLY";
public static final Set<String> CALC_TYPES = Set.of(CALC_SIMPLE, CALC_MONTHLY, CALC_QUARTERLY, CALC_HALF_YEARLY, CALC_YEARLY);
/** RBI: deposits with other banks within 20% of total deposits as on the previous 31 March, and within 5% with one bank. */
public static final BigDecimal GROSS_LIMIT_PERCENT = BigDecimal.valueOf(20);
public static final BigDecimal BANK_LIMIT_PERCENT = BigDecimal.valueOf(5);
/** Current accounts with banks also count towards the 20% inter-bank exposure. */
public static final String BANK_BALANCE_HEADER_GL_CODE = "100200";
/** Deposits of the bank, for the suggested total-deposits figure: everything under the deposit header, plus RD. */
public static final String DEPOSIT_HEADER_GL_CODE = "200100";
public static final String RD_DEPOSIT_GL_CODE = "200201";
public static final String PARTY_ID = "partyId";
public static final String CERT_NO = "certNo";
public static final String CERT_DATE = "certDate";
public static final String DEAL_DATE = "dealDate";
public static final String AMOUNT = "amount";
public static final String INT_RATE = "intRate";
public static final String MATURITY_DATE = "maturityDate";
public static final String PAYOUT = "payout";
public static final String CALC_TYPE = "calcType";
public static final String BANK_GL_ACCOUNT_ID = "bankGlAccountId";
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 LOCALE = "locale";
public static final String DATE_FORMAT = "dateFormat";
public static final String CLOSURE_MATURITY = "MATURITY";
public static final String CLOSURE_PREMATURE = "PREMATURE";
public static final String CLOSURE_RENEWAL = "RENEWAL";
public static final String LINE_PROVISION = "PROVISION";
public static final String LINE_PAYOUT = "PAYOUT";
public static final String INTEREST_RECEIVABLE_GL_CODE = "100603";
/** Tax the other bank deducts from the interest, recoverable from the tax department. */
public static final String TDS_RECEIVABLE_GL_CODE = "100602";
public static final String CLOSE_DATE = "closeDate";
public static final String RECEIVED_AMOUNT = "receivedAmount";
public static final String TDS = "tds";
public static final String PENAL_AMOUNT = "penalAmount";
public static final String PRINCIPAL_BANK_GL_ACCOUNT_ID = "principalBankGlAccountId";
public static final String INTEREST_BANK_GL_ACCOUNT_ID = "interestBankGlAccountId";
public static final String NEW_CERT_NO = "newCertNo";
public static final String NEW_CERT_DATE = "newCertDate";
public static final String NEW_INT_RATE = "newIntRate";
public static final String NEW_MATURITY_DATE = "newMaturityDate";
public static final String NEW_PAYOUT = "newPayout";
public static final String NEW_CALC_TYPE = "newCalcType";
public static final String MATURITY_AMOUNT = "maturityAmount";
public static final String RENEWAL_AMOUNT = "renewalAmount";
public static final String DIFF_BANK_GL_ACCOUNT_ID = "diffBankGlAccountId";
public static final String LINE_MATURITY = "MATURITY";
public static final String LINE_TYPE = "lineType";
public static final String INT_DATE = "intDate";
/** Reminders show lines due within this many days (as for Govt securities). */
public static final int DUE_WITHIN_DAYS = 2;
public static final Set<String> INTEREST_PARAMS = Set.of(LINE_TYPE, INT_DATE, RECEIVED_AMOUNT, TDS, BANK_GL_ACCOUNT_ID, LOCALE,
DATE_FORMAT);
public static final Set<String> MATURE_PARAMS = Set.of(RECEIVED_AMOUNT, TDS, PRINCIPAL_BANK_GL_ACCOUNT_ID, INTEREST_BANK_GL_ACCOUNT_ID,
REMARKS, LOCALE, DATE_FORMAT);
public static final Set<String> PREMATURE_PARAMS = Set.of(CLOSE_DATE, RECEIVED_AMOUNT, TDS, PENAL_AMOUNT, PRINCIPAL_BANK_GL_ACCOUNT_ID,
INTEREST_BANK_GL_ACCOUNT_ID, REMARKS, LOCALE, DATE_FORMAT);
public static final Set<String> RENEW_PARAMS = Set.of(NEW_CERT_NO, NEW_CERT_DATE, NEW_INT_RATE, NEW_MATURITY_DATE, NEW_PAYOUT,
NEW_CALC_TYPE, MATURITY_AMOUNT, RENEWAL_AMOUNT, TDS, DIFF_BANK_GL_ACCOUNT_ID, REMARKS, LOCALE, DATE_FORMAT);
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);
}
@@ -0,0 +1,186 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.investment.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.investment.data.InvestmentFdClosureData;
import org.apache.fineract.portfolio.investment.data.InvestmentFdData;
import org.apache.fineract.portfolio.investment.data.InvestmentFdDueData;
import org.apache.fineract.portfolio.investment.data.InvestmentFdScheduleData;
import org.apache.fineract.portfolio.investment.service.InvestmentFdClosureService;
import org.apache.fineract.portfolio.investment.service.InvestmentFdInterestService;
import org.apache.fineract.portfolio.investment.service.InvestmentFdService;
import org.springframework.stereotype.Component;
@Path("/v1/investment/fds")
@Component
@Tag(name = "Investment FD", description = "Fixed deposits the bank places with other banks.")
@RequiredArgsConstructor
public class InvestmentFdApiResource {
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-yyyy");
private final PlatformSecurityContext context;
private final InvestmentFdService fdService;
private final InvestmentFdClosureService closureService;
private final InvestmentFdInterestService interestService;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
@GET
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "All FDs placed with other banks")
public List<InvestmentFdData> retrieveAll() {
this.context.authenticatedUser().validateHasReadPermission(InvestmentFdApiConstants.ENTITY);
return this.fdService.retrieveAll();
}
@GET
@Path("quote")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Preview an FD: interest, maturity amount and the RBI limits", description = "Dates dd-MM-yyyy")
public InvestmentFdData quote(@QueryParam("partyId") final Long partyId, @QueryParam("amount") final BigDecimal amount,
@QueryParam("intRate") final BigDecimal intRate, @QueryParam("dealDate") final String dealDate,
@QueryParam("maturityDate") final String maturityDate, @QueryParam("payout") final String payout,
@QueryParam("calcType") final String calcType) {
this.context.authenticatedUser().validateHasReadPermission(InvestmentFdApiConstants.ENTITY);
return this.fdService.quote(partyId, amount, intRate, date(dealDate), date(maturityDate), payout, calcType);
}
@GET
@Path("due")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "FD interest vouchers and maturities due", description = "Due now or within withinDays (default 2)")
public List<InvestmentFdDueData> due(@QueryParam("withinDays") final Integer withinDays) {
this.context.authenticatedUser().validateHasReadPermission(InvestmentFdApiConstants.ENTITY);
return this.interestService.due(withinDays == null ? InvestmentFdApiConstants.DUE_WITHIN_DAYS : withinDays);
}
@GET
@Path("{fdId}/interest-schedule")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Interest lines of an FD", description = "31 Mar / 30 Sep provisions, payouts and maturity")
public InvestmentFdScheduleData schedule(@PathParam("fdId") final Long fdId) {
this.context.authenticatedUser().validateHasReadPermission(InvestmentFdApiConstants.ENTITY);
return this.interestService.schedule(fdId);
}
@POST
@Path("{fdId}/interest")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Post the next interest line of an FD", description = "lineType PROVISION | PAYOUT, intDate; payout: receivedAmount, tds")
public CommandProcessingResult postInterest(@PathParam("fdId") final Long fdId, @Parameter(hidden = true) final String apiRequestBodyAsJson) {
return this.commandsSourceWritePlatformService
.logCommandSource(new CommandWrapperBuilder().postInvestmentFdInterest(fdId).withJson(apiRequestBodyAsJson).build());
}
@GET
@Path("{fdId}")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "One FD")
public InvestmentFdData retrieveOne(@PathParam("fdId") @Parameter(description = "fdId") final Long fdId) {
this.context.authenticatedUser().validateHasReadPermission(InvestmentFdApiConstants.ENTITY);
return this.fdService.retrieveOne(fdId);
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Place an FD", description = "Dr the bank's FD GL / Cr our bank GL, dated the deal date; refused beyond the RBI limits.")
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper request = new CommandWrapperBuilder().createInvestmentFd().withJson(apiRequestBodyAsJson).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
@GET
@Path("{fdId}/closure-quote")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Preview a maturity, premature closure or renewal", description = "type MATURITY | PREMATURE | RENEWAL")
public InvestmentFdClosureData closureQuote(@PathParam("fdId") final Long fdId, @QueryParam("type") final String type,
@QueryParam("closeDate") final String closeDate, @QueryParam("receivedAmount") final BigDecimal receivedAmount,
@QueryParam("tds") final BigDecimal tds, @QueryParam("renewalAmount") final BigDecimal renewalAmount) {
this.context.authenticatedUser().validateHasReadPermission(InvestmentFdApiConstants.ENTITY);
return this.closureService.quote(fdId, type, date(closeDate), receivedAmount, tds, renewalAmount);
}
@POST
@Path("{fdId}/mature")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Close an FD at maturity", description = "Dated the maturity date; amount as credited by the bank, optional TDS.")
public CommandProcessingResult mature(@PathParam("fdId") final Long fdId, @Parameter(hidden = true) final String apiRequestBodyAsJson) {
return this.commandsSourceWritePlatformService
.logCommandSource(new CommandWrapperBuilder().matureInvestmentFd(fdId).withJson(apiRequestBodyAsJson).build());
}
@POST
@Path("{fdId}/premature")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Close an FD before maturity", description = "Not calculated: the amount received (and any penal interest) is entered.")
public CommandProcessingResult premature(@PathParam("fdId") final Long fdId, @Parameter(hidden = true) final String apiRequestBodyAsJson) {
return this.commandsSourceWritePlatformService
.logCommandSource(new CommandWrapperBuilder().prematureInvestmentFd(fdId).withJson(apiRequestBodyAsJson).build());
}
@POST
@Path("{fdId}/renew")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Renew an FD at maturity", description = "New certificate, rate and maturity; money added or taken out goes through a bank GL.")
public CommandProcessingResult renew(@PathParam("fdId") final Long fdId, @Parameter(hidden = true) final String apiRequestBodyAsJson) {
return this.commandsSourceWritePlatformService
.logCommandSource(new CommandWrapperBuilder().renewInvestmentFd(fdId).withJson(apiRequestBodyAsJson).build());
}
@POST
@Path("deposit-base")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Record total deposits as on a 31 March", description = "The base of the RBI limits for the following year.")
public CommandProcessingResult setDepositBase(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper request = new CommandWrapperBuilder().setInvestmentDepositBase().withJson(apiRequestBodyAsJson).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
private static LocalDate date(final String value) {
return StringUtils.isBlank(value) ? null : LocalDate.parse(value.trim(), DATE);
}
}
@@ -0,0 +1,65 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.investment.data;
import java.math.BigDecimal;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
/** What closing an FD (maturity, premature or renewal) works out to, and the voucher it posts. */
@Getter
@Builder
public class InvestmentFdClosureData {
private final Long fdId;
private final String dealNo;
private final String type;
private final String closeDate;
private final BigDecimal principal;
/** Interest the system works out for the period, to compare with the bank's figure. */
private final BigDecimal expectedInterest;
/** Interest already booked by provisions and not yet received. */
private final BigDecimal provisioned;
/** Money credited to our bank by the other bank (net of TDS). */
private final BigDecimal receivedAmount;
private final BigDecimal tds;
private final BigDecimal penalAmount;
/** Gross interest as per the bank: received + TDS − principal. */
private final BigDecimal interest;
/** Interest income of this entry: interest less what was provisioned (negative = reversed from income). */
private final BigDecimal income;
/** Bank's interest less the system's. */
private final BigDecimal difference;
private final BigDecimal renewalAmount;
/** Renewal: money added (positive) or taken out (negative) against the maturity amount. */
private final BigDecimal topUp;
private final List<Line> lines;
@Getter
@AllArgsConstructor
public static class Line {
private final String side;
private final String glCode;
private final String glName;
private final BigDecimal amount;
}
}
@@ -0,0 +1,103 @@
/**
* 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.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import lombok.Builder;
import lombok.Getter;
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
import org.apache.fineract.portfolio.investment.domain.InvestmentFd;
/** An FD placed with another bank; also the shape of a purchase preview (with the limit check). */
@Getter
@Builder
public class InvestmentFdData {
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-yyyy");
private final Long id;
private final String dealNo;
private final Long partyId;
private final String partyCode;
private final String partyName;
private final String certNo;
private final String certDate;
private final String dealDate;
private final BigDecimal principal;
private final BigDecimal intRate;
private final String maturityDate;
private final long days;
private final String payout;
private final String calcType;
private final BigDecimal expectedInterest;
private final BigDecimal expectedMaturityAmount;
/** Interest paid each half year / year for a payout FD. */
private final BigDecimal periodInterest;
private final String fdGlCode;
private final String fdGlName;
private final String interestGlCode;
private final String interestGlName;
private final Long bankGlAccountId;
private final String bankGlCode;
private final String bankGlName;
private final String status;
private final String remarks;
private final String glTransactionId;
private final String voucherNo;
private final String renewedFromDealNo;
private final String closedOnDate;
private final String closureType;
private final BigDecimal closureAmount;
private final BigDecimal closureInterest;
private final BigDecimal closureTds;
private final BigDecimal closurePenal;
private final String closureVoucherNo;
private final String closureGlTransactionId;
private final InvestmentFdLimitData limit;
public static InvestmentFdData from(final InvestmentFd f, final BigDecimal periodInterest) {
return builder().id(f.getId()).dealNo(f.getDealNo()).partyId(f.getParty().getId()).partyCode(f.getParty().getPartyCode())
.partyName(f.getParty().getPartyName()).certNo(f.getCertNo()).certDate(format(f.getCertDate()))
.dealDate(format(f.getDealDate())).principal(f.getPrincipal()).intRate(f.getIntRate())
.maturityDate(format(f.getMaturityDate())).days(ChronoUnit.DAYS.between(f.getDealDate(), f.getMaturityDate()))
.payout(f.getPayout()).calcType(f.getCalcType()).expectedInterest(f.getExpectedInterest())
.expectedMaturityAmount(f.getExpectedMaturityAmount()).periodInterest(periodInterest).fdGlCode(code(f.getFdGlAccount()))
.fdGlName(name(f.getFdGlAccount())).interestGlCode(code(f.getInterestGlAccount())).interestGlName(name(f.getInterestGlAccount()))
.bankGlAccountId(f.getBankGlAccount().getId()).bankGlCode(code(f.getBankGlAccount())).bankGlName(name(f.getBankGlAccount()))
.status(f.getStatus()).remarks(f.getRemarks()).glTransactionId(f.getGlTransactionId()).voucherNo(f.getVoucherNo())
.renewedFromDealNo(f.getRenewedFrom() == null ? null : f.getRenewedFrom().getDealNo()).closedOnDate(format(f.getClosedOnDate()))
.closureType(f.getClosureType()).closureAmount(f.getClosureAmount()).closureInterest(f.getClosureInterest())
.closureTds(f.getClosureTds()).closurePenal(f.getClosurePenal()).closureVoucherNo(f.getClosureVoucherNo()).closureGlTransactionId(f.getClosureGlTransactionId()).build();
}
private static String code(final GLAccount gl) {
return gl == null ? null : gl.getGlCode();
}
private static String name(final GLAccount gl) {
return gl == null ? null : gl.getName();
}
private static String format(final LocalDate date) {
return date == null ? null : date.format(DATE);
}
}
@@ -0,0 +1,40 @@
/**
* 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;
/** The next interest voucher (or the maturity) of an FD that is due, or due within the reminder window. */
@Getter
@Builder
public class InvestmentFdDueData {
private final Long fdId;
private final String dealNo;
private final String partyName;
private final String certNo;
/** PROVISION, PAYOUT or MATURITY. */
private final String lineType;
private final String intDate;
private final BigDecimal amount;
private final boolean postable;
private final boolean overdue;
}
@@ -0,0 +1,46 @@
/**
* 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 new FD stands against the RBI limits on deposits with other banks. */
@Getter
@Builder
public class InvestmentFdLimitData {
/** The 31 March the limits are based on. */
private final String asOnDate;
/** Total deposits as on that date, as entered; empty until someone enters it. */
private final BigDecimal totalDeposits;
/** The same figure worked out from the deposit GLs, as a suggestion. */
private final BigDecimal glTotalDeposits;
private final BigDecimal grossLimit;
private final BigDecimal fdOutstanding;
private final BigDecimal bankBalances;
private final BigDecimal grossAfter;
private final BigDecimal bankLimit;
private final String partyName;
private final BigDecimal bankAfter;
private final BigDecimal newAmount;
private final boolean withinLimits;
private final String message;
}
@@ -0,0 +1,58 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.investment.data;
import java.math.BigDecimal;
import java.util.List;
import lombok.Builder;
import lombok.Getter;
/** Interest lines of an FD: 31 Mar / 30 Sep provisions, payouts of a half-yearly / yearly FD, and maturity. */
@Getter
@Builder
public class InvestmentFdScheduleData {
private final InvestmentFdData fd;
/** Provisioned interest not yet received (interest receivable of this FD). */
private final BigDecimal provisioned;
private final List<Row> rows;
@Getter
@Builder
public static class Row {
/** PROVISION, PAYOUT or MATURITY (shown for information; closed on the Maturity screen). */
private final String lineType;
private final String intDate;
private final String fromDate;
private final String toDate;
private final long days;
private final BigDecimal grossInterest;
/** Payout: the part already provisioned, cleared from interest receivable. */
private final BigDecimal alreadyBooked;
private final BigDecimal tds;
/** Provision: interest booked. Payout: interest income of the payout (gross less already booked). */
private final BigDecimal amount;
private final boolean posted;
private final String voucherNo;
private final String glTransactionId;
/** The next line to post, and its date has come. */
private final boolean postable;
}
}
@@ -42,6 +42,9 @@ public class InvestmentPartyData {
private final Long glAccountId;
private final String glCode;
private final String glName;
private final Long interestGlAccountId;
private final String interestGlCode;
private final String interestGlName;
private final String rbiCode;
private final BigDecimal limit;
private final String leiNo;
@@ -56,6 +59,9 @@ public class InvestmentPartyData {
.partyName(p.getPartyName()).address1(p.getAddress1()).address2(p.getAddress2()).address3(p.getAddress3())
.city(p.getCity()).pincode(p.getPincode()).phone(p.getPhone()).fax(p.getFax()).bankType(p.getBankType())
.glAccountId(p.getGlAccount().getId()).glCode(p.getGlAccount().getGlCode()).glName(p.getGlAccount().getName())
.interestGlAccountId(p.getInterestGlAccount() == null ? null : p.getInterestGlAccount().getId())
.interestGlCode(p.getInterestGlAccount() == null ? null : p.getInterestGlAccount().getGlCode())
.interestGlName(p.getInterestGlAccount() == null ? null : p.getInterestGlAccount().getName())
.rbiCode(p.getRbiCode()).limit(p.getExposureLimit()).leiNo(p.getLeiNo()).cpBankName(p.getCpBankName())
.cpBranchName(p.getCpBranchName()).cpAccountNo(p.getCpAccountNo()).cpIfsc(p.getCpIfsc()).active(p.isActive()).build();
}
@@ -0,0 +1,47 @@
/**
* 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.Table;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
/** The bank's total deposits as on a 31 March: the base for the RBI limits on deposits placed with other banks. */
@Entity
@Table(name = "m_investment_deposit_base")
@Getter
@Setter
@NoArgsConstructor
public class InvestmentDepositBase extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@Column(name = "as_on_date", nullable = false, unique = true)
private LocalDate asOnDate;
@Column(name = "total_deposits", scale = 6, precision = 19, nullable = false)
private BigDecimal totalDeposits;
@Column(name = "remarks", length = 500)
private String remarks;
}
@@ -0,0 +1,28 @@
/**
* 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.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
public interface InvestmentDepositBaseRepository extends JpaRepository<InvestmentDepositBase, Long> {
Optional<InvestmentDepositBase> findByAsOnDate(LocalDate asOnDate);
}
@@ -0,0 +1,135 @@
/**
* 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 fixed deposit the bank has placed with another bank, from placement to maturity, premature closure or renewal. */
@Entity
@Table(name = "m_investment_fd")
@Getter
@Setter
@NoArgsConstructor
public class InvestmentFd extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@Column(name = "deal_no", nullable = false, length = 20, unique = true)
private String dealNo;
@ManyToOne
@JoinColumn(name = "party_id", nullable = false)
private InvestmentParty party;
/** The bank's FD GL (from the party), debited when the money is placed. */
@ManyToOne
@JoinColumn(name = "fd_gl_account_id", nullable = false)
private GLAccount fdGlAccount;
@ManyToOne
@JoinColumn(name = "interest_gl_account_id", nullable = false)
private GLAccount interestGlAccount;
/** Certificate (receipt) number issued by the other bank. */
@Column(name = "cert_no", nullable = false, length = 50)
private String certNo;
@Column(name = "cert_date")
private LocalDate certDate;
@Column(name = "deal_date", nullable = false)
private LocalDate dealDate;
@Column(name = "principal", scale = 6, precision = 19, nullable = false)
private BigDecimal principal;
@Column(name = "int_rate", scale = 6, precision = 19, nullable = false)
private BigDecimal intRate;
@Column(name = "maturity_date", nullable = false)
private LocalDate maturityDate;
@Column(name = "payout", nullable = false, length = 20)
private String payout;
@Column(name = "calc_type", nullable = false, length = 20)
private String calcType;
@Column(name = "expected_interest", scale = 6, precision = 19)
private BigDecimal expectedInterest;
@Column(name = "expected_maturity_amount", scale = 6, precision = 19)
private BigDecimal expectedMaturityAmount;
/** Our bank account the money was paid from. */
@ManyToOne
@JoinColumn(name = "bank_gl_account_id", nullable = false)
private GLAccount bankGlAccount;
/** The FD this one renews, if any. */
@ManyToOne
@JoinColumn(name = "renewed_from_id")
private InvestmentFd renewedFrom;
@Column(name = "status", nullable = false, length = 20)
private String status;
@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;
@Column(name = "closed_on_date")
private LocalDate closedOnDate;
/** MATURITY, PREMATURE or RENEWAL. */
@Column(name = "closure_type", length = 20)
private String closureType;
@Column(name = "closure_amount", scale = 6, precision = 19)
private BigDecimal closureAmount;
@Column(name = "closure_interest", scale = 6, precision = 19)
private BigDecimal closureInterest;
@Column(name = "closure_tds", scale = 6, precision = 19)
private BigDecimal closureTds;
@Column(name = "closure_penal", scale = 6, precision = 19)
private BigDecimal closurePenal;
@Column(name = "closure_gl_transaction_id", length = 50)
private String closureGlTransactionId;
@Column(name = "closure_voucher_no", length = 30)
private String closureVoucherNo;
}
@@ -0,0 +1,87 @@
/**
* 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 posted interest line of an FD: a 31 Mar / 30 Sep provision, or a half-yearly / yearly payout. */
@Entity
@Table(name = "m_investment_fd_interest")
@Getter
@Setter
@NoArgsConstructor
public class InvestmentFdInterest extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@ManyToOne
@JoinColumn(name = "fd_id", nullable = false)
private InvestmentFd fd;
/** PROVISION or PAYOUT. */
@Column(name = "line_type", nullable = false, length = 20)
private String lineType;
@Column(name = "int_date", nullable = false)
private LocalDate intDate;
@Column(name = "from_date")
private LocalDate fromDate;
@Column(name = "to_date")
private LocalDate toDate;
@Column(name = "days")
private Integer days;
@Column(name = "gross_interest", scale = 6, precision = 19)
private BigDecimal grossInterest;
/** For a payout: the part already provisioned, cleared from interest receivable. */
@Column(name = "already_booked", scale = 6, precision = 19)
private BigDecimal alreadyBooked;
@Column(name = "tds", scale = 6, precision = 19)
private BigDecimal tds;
@Column(name = "amount", scale = 6, precision = 19, nullable = false)
private BigDecimal amount;
@ManyToOne
@JoinColumn(name = "bank_gl_account_id")
private GLAccount bankGlAccount;
@Column(name = "gl_transaction_id", length = 50)
private String glTransactionId;
@Column(name = "voucher_no", length = 30)
private String voucherNo;
@Column(name = "posted_on_date")
private LocalDate postedOnDate;
}
@@ -0,0 +1,27 @@
/**
* 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.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface InvestmentFdInterestRepository extends JpaRepository<InvestmentFdInterest, Long> {
List<InvestmentFdInterest> findByFdIdOrderByIntDateAscIdAsc(Long fdId);
}
@@ -0,0 +1,38 @@
/**
* 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.LockModeType;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface InvestmentFdRepository extends JpaRepository<InvestmentFd, Long> {
@Lock(value = LockModeType.PESSIMISTIC_WRITE)
@Query("select f from InvestmentFd f where f.id = :fdId")
Optional<InvestmentFd> findByIdWithLock(@Param("fdId") Long fdId);
List<InvestmentFd> findByStatusOrderByIdAsc(String status);
boolean existsByPartyIdAndCertNoIgnoreCase(Long partyId, String certNo);
}
@@ -78,6 +78,11 @@ public class InvestmentParty extends AbstractAuditableWithUTCDateTimeCustom<Long
@JoinColumn(name = "gl_account_id", nullable = false)
private GLAccount glAccount;
/** Interest income GL of the FDs placed with this bank (Bond Type FD only). */
@ManyToOne
@JoinColumn(name = "interest_gl_account_id")
private GLAccount interestGlAccount;
@Column(name = "rbi_code", length = 30)
private String rbiCode;
@@ -20,4 +20,9 @@ package org.apache.fineract.portfolio.investment.domain;
import org.springframework.data.jpa.repository.JpaRepository;
public interface InvestmentPartyRepository extends JpaRepository<InvestmentParty, Long> {}
public interface InvestmentPartyRepository extends JpaRepository<InvestmentParty, Long> {
/** Locks the bank so two FDs with it are checked against the 5% limit one after the other. */
@org.springframework.data.jpa.repository.Lock(value = jakarta.persistence.LockModeType.PESSIMISTIC_WRITE)
@org.springframework.data.jpa.repository.Query("select p from InvestmentParty p where p.id = :partyId")
java.util.Optional<InvestmentParty> findByIdWithLock(@org.springframework.data.repository.query.Param("partyId") Long partyId);}
@@ -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.InvestmentFdService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "INVESTMENTFD", action = "CREATE")
@RequiredArgsConstructor
public class CreateInvestmentFdCommandHandler implements NewCommandSourceHandler {
private final InvestmentFdService fdService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.fdService.create(command);
}
}
@@ -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.InvestmentFdClosureService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "INVESTMENTFD", action = "MATURE")
@RequiredArgsConstructor
public class MatureInvestmentFdCommandHandler implements NewCommandSourceHandler {
private final InvestmentFdClosureService closureService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.closureService.mature(command.entityId(), command);
}
}
@@ -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.InvestmentFdInterestService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "INVESTMENTFD", action = "POSTINTEREST")
@RequiredArgsConstructor
public class PostInvestmentFdInterestCommandHandler implements NewCommandSourceHandler {
private final InvestmentFdInterestService interestService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.interestService.post(command.entityId(), command);
}
}
@@ -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.InvestmentFdClosureService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "INVESTMENTFD", action = "PREMATURE")
@RequiredArgsConstructor
public class PrematureInvestmentFdCommandHandler implements NewCommandSourceHandler {
private final InvestmentFdClosureService closureService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.closureService.premature(command.entityId(), command);
}
}
@@ -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.InvestmentFdClosureService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "INVESTMENTFD", action = "RENEW")
@RequiredArgsConstructor
public class RenewInvestmentFdCommandHandler implements NewCommandSourceHandler {
private final InvestmentFdClosureService closureService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.closureService.renew(command.entityId(), command);
}
}
@@ -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.InvestmentFdService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "INVESTMENTFD", action = "SETBASE")
@RequiredArgsConstructor
public class SetInvestmentDepositBaseCommandHandler implements NewCommandSourceHandler {
private final InvestmentFdService fdService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.fdService.setDepositBase(command);
}
}
@@ -36,6 +36,7 @@ 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.INTEREST_GL_ACCOUNT_ID;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.INT_RATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ISIN;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ISSUE_DATE;
@@ -99,6 +100,10 @@ public final class InvestmentMasterCommandFromApiJsonDeserializer {
validateBondType(element, v, create);
validateRequiredString(element, v, PARTY_NAME, 150, create);
validateGlAccountId(element, v, create);
if (this.fromApiJsonHelper.parameterExists(INTEREST_GL_ACCOUNT_ID, element)) {
v.reset().parameter(INTEREST_GL_ACCOUNT_ID).value(this.fromApiJsonHelper.extractLongNamed(INTEREST_GL_ACCOUNT_ID, element))
.ignoreIfNull().longGreaterThanZero();
}
validateAddress(element, v);
validateOptionalString(element, v, BANK_TYPE, 60);
validateOptionalString(element, v, RBI_CODE, 30);
@@ -0,0 +1,79 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.investment.service;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.CALC_HALF_YEARLY;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.CALC_MONTHLY;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.CALC_QUARTERLY;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.CALC_SIMPLE;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.CALC_YEARLY;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.PAYOUT_AT_MATURITY;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.PAYOUT_HALF_YEARLY;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
/**
* Interest on an FD with another bank, on actual days over a 365-day year. A payout FD (interest paid every half
* year / year) earns simple interest; an FD paid at maturity compounds at the chosen frequency, or is simple. The
* user also enters the bank's own figure at maturity, so a different compounding shows up as a difference.
*/
public final class InvestmentFdCalculator {
private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);
private static final BigDecimal DAYS_IN_YEAR = BigDecimal.valueOf(365);
private InvestmentFdCalculator() {}
public static BigDecimal interest(final BigDecimal principal, final BigDecimal ratePercent, final LocalDate from, final LocalDate to,
final String calcType, final String payout) {
final long days = ChronoUnit.DAYS.between(from, to);
if (days <= 0 || principal == null || ratePercent == null || principal.signum() <= 0 || ratePercent.signum() <= 0) {
return BigDecimal.ZERO;
}
if (!PAYOUT_AT_MATURITY.equals(payout) || CALC_SIMPLE.equals(calcType)) {
return principal.multiply(ratePercent).multiply(BigDecimal.valueOf(days)).divide(HUNDRED.multiply(DAYS_IN_YEAR), 2,
RoundingMode.HALF_UP);
}
final int n = periodsPerYear(calcType);
final double factor = Math.pow(1 + ratePercent.doubleValue() / 100 / n, n * (days / 365.0));
return principal.multiply(BigDecimal.valueOf(factor - 1)).setScale(2, RoundingMode.HALF_UP);
}
/** Interest paid each half year or year on a payout FD; zero for an FD paid at maturity. */
public static BigDecimal periodInterest(final BigDecimal principal, final BigDecimal ratePercent, final String payout) {
if (PAYOUT_AT_MATURITY.equals(payout) || principal == null || ratePercent == null) {
return BigDecimal.ZERO;
}
final BigDecimal perYear = principal.multiply(ratePercent).divide(HUNDRED, 2, RoundingMode.HALF_UP);
return PAYOUT_HALF_YEARLY.equals(payout) ? perYear.divide(BigDecimal.valueOf(2), 2, RoundingMode.HALF_UP) : perYear;
}
private static int periodsPerYear(final String calcType) {
return switch (calcType) {
case CALC_MONTHLY -> 12;
case CALC_QUARTERLY -> 4;
case CALC_HALF_YEARLY -> 2;
case CALC_YEARLY -> 1;
default -> throw new IllegalArgumentException("Unknown calculation type " + calcType);
};
}
}
@@ -0,0 +1,463 @@
/**
* 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.InvestmentFdApiConstants.CALC_TYPES;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.CLOSE_DATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.CLOSURE_MATURITY;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.CLOSURE_PREMATURE;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.CLOSURE_RENEWAL;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.DIFF_BANK_GL_ACCOUNT_ID;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.INTEREST_BANK_GL_ACCOUNT_ID;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.INTEREST_RECEIVABLE_GL_CODE;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.LINE_PAYOUT;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.LINE_PROVISION;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.MATURE_PARAMS;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.MATURITY_AMOUNT;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.NEW_CALC_TYPE;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.NEW_CERT_DATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.NEW_CERT_NO;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.NEW_INT_RATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.NEW_MATURITY_DATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.NEW_PAYOUT;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.PAYOUTS;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.PENAL_AMOUNT;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.PREMATURE_PARAMS;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.PRINCIPAL_BANK_GL_ACCOUNT_ID;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.RECEIVED_AMOUNT;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.REMARKS;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.RENEWAL_AMOUNT;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.RENEW_PARAMS;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.STATUS_ACTIVE;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.STATUS_MATURED;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.STATUS_PREMATURE;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.STATUS_RENEWED;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.TDS;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.TDS_RECEIVABLE_GL_CODE;
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 java.util.Set;
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.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.data.InvestmentFdClosureData;
import org.apache.fineract.portfolio.investment.data.InvestmentFdLimitData;
import org.apache.fineract.portfolio.investment.domain.InvestmentFd;
import org.apache.fineract.portfolio.investment.domain.InvestmentFdInterest;
import org.apache.fineract.portfolio.investment.domain.InvestmentFdInterestRepository;
import org.apache.fineract.portfolio.investment.domain.InvestmentFdRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* Closing an FD placed with another bank (Jitendra Sir). At maturity the system works out the interest and the user
* enters what the bank actually paid; the two are shown side by side, so a different compounding shows up. A premature
* closure is not calculated: the user enters the amount received (and any penal interest). The interest is received
* plus TDS less the principal; what was already provisioned clears interest receivable and the rest is income (a
* shortfall is reversed from income). Renewal closes the FD and opens a new certificate on the maturity date: renewed as
* it is, no money moves; money added or taken out goes through the bank GL the user names. TDS, when the bank deducts
* it, goes to TDS receivable.
*/
@Service
@RequiredArgsConstructor
public class InvestmentFdClosureService {
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-yyyy");
private final FromJsonHelper fromApiJsonHelper;
private final InvestmentFdRepository fdRepository;
private final InvestmentFdInterestRepository interestRepository;
private final GLAccountRepositoryWrapper glAccountRepository;
private final InvestmentJournalPoster journalPoster;
private final InvestmentFdService fdService;
private final InvestmentFdInterestService interestService;
/** Preview of a closure; nothing is posted. */
@Transactional(readOnly = true)
public InvestmentFdClosureData quote(final Long fdId, final String type, final LocalDate closeDate, final BigDecimal receivedAmount,
final BigDecimal tds, final BigDecimal renewalAmount) {
final InvestmentFd f = this.fdRepository.findById(fdId).orElseThrow(() -> InvestmentFdService.rule("not.found", "FD " + fdId + " does not exist"));
final String t = CLOSURE_PREMATURE.equals(type) || CLOSURE_RENEWAL.equals(type) ? type : CLOSURE_MATURITY;
final LocalDate date = CLOSURE_PREMATURE.equals(t) ? (closeDate == null ? DateUtils.getBusinessLocalDate() : closeDate) : f.getMaturityDate();
final Figures x = figures(f, t, date, receivedAmount, tds, renewalAmount);
final Entry e = CLOSURE_RENEWAL.equals(t) ? renewalEntry(f, x, f.getBankGlAccount()) : closureEntry(f, x, f.getBankGlAccount(), f.getBankGlAccount());
return x.toData(f, t, date, null, e);
}
@Transactional
public CommandProcessingResult mature(final Long fdId, final JsonCommand command) {
validate(command.json(), MATURE_PARAMS, (v, el) -> {
amountOptional(v, el, RECEIVED_AMOUNT);
amountOptional(v, el, TDS);
});
final InvestmentFd f = active(fdId);
final LocalDate date = f.getMaturityDate();
if (date.isAfter(DateUtils.getBusinessLocalDate())) {
throw InvestmentFdService.rule("mature.before.date", "FD " + f.getDealNo() + " matures on " + date.format(DATE)
+ "; close it before then as a premature closure");
}
return close(f, command, CLOSURE_MATURITY, date, STATUS_MATURED);
}
@Transactional
public CommandProcessingResult premature(final Long fdId, final JsonCommand command) {
validate(command.json(), PREMATURE_PARAMS, (v, el) -> {
v.reset().parameter(CLOSE_DATE).value(this.fromApiJsonHelper.extractLocalDateNamed(CLOSE_DATE, el)).notNull();
v.reset().parameter(RECEIVED_AMOUNT).value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(RECEIVED_AMOUNT, el)).notNull()
.positiveAmount();
amountOptional(v, el, TDS);
amountOptional(v, el, PENAL_AMOUNT);
});
final InvestmentFd f = active(fdId);
final LocalDate date = command.localDateValueOfParameterNamed(CLOSE_DATE);
if (date.isAfter(DateUtils.getBusinessLocalDate())) {
throw InvestmentFdService.rule("close.future", "The closure date cannot be after today");
}
if (!date.isAfter(f.getDealDate()) || !date.isBefore(f.getMaturityDate())) {
throw InvestmentFdService.rule("premature.date", "A premature closure falls after the deal date " + f.getDealDate().format(DATE)
+ " and before maturity " + f.getMaturityDate().format(DATE));
}
return close(f, command, CLOSURE_PREMATURE, date, STATUS_PREMATURE);
}
@Transactional
public CommandProcessingResult renew(final Long fdId, final JsonCommand command) {
validate(command.json(), RENEW_PARAMS, (v, el) -> {
v.reset().parameter(NEW_CERT_NO).value(this.fromApiJsonHelper.extractStringNamed(NEW_CERT_NO, el)).notBlank().notExceedingLengthOf(50);
v.reset().parameter(NEW_INT_RATE).value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(NEW_INT_RATE, el)).notNull()
.positiveAmount();
v.reset().parameter(NEW_MATURITY_DATE).value(this.fromApiJsonHelper.extractLocalDateNamed(NEW_MATURITY_DATE, el)).notNull();
v.reset().parameter(NEW_PAYOUT).value(this.fromApiJsonHelper.extractStringNamed(NEW_PAYOUT, el)).ignoreIfNull()
.isOneOfTheseValues(PAYOUTS.toArray());
v.reset().parameter(NEW_CALC_TYPE).value(this.fromApiJsonHelper.extractStringNamed(NEW_CALC_TYPE, el)).ignoreIfNull()
.isOneOfTheseValues(CALC_TYPES.toArray());
amountOptional(v, el, MATURITY_AMOUNT);
amountOptional(v, el, RENEWAL_AMOUNT);
amountOptional(v, el, TDS);
});
final InvestmentFd f = active(fdId);
final LocalDate date = f.getMaturityDate();
if (date.isAfter(DateUtils.getBusinessLocalDate())) {
throw InvestmentFdService.rule("renew.before.date", "FD " + f.getDealNo() + " can be renewed on or after its maturity " + date.format(DATE));
}
final LocalDate newMaturity = command.localDateValueOfParameterNamed(NEW_MATURITY_DATE);
if (!newMaturity.isAfter(date)) {
throw InvestmentFdService.rule("renew.maturity", "The new maturity date must be after " + date.format(DATE));
}
final String newCertNo = command.stringValueOfParameterNamed(NEW_CERT_NO).trim();
if (this.fdRepository.existsByPartyIdAndCertNoIgnoreCase(f.getParty().getId(), newCertNo)) {
throw InvestmentFdService.rule("cert.duplicate", "Certificate " + newCertNo + " of " + f.getParty().getPartyName() + " is already entered");
}
final Figures x = figures(f, CLOSURE_RENEWAL, date, amount(command, MATURITY_AMOUNT), amount(command, TDS), amount(command, RENEWAL_AMOUNT));
if (x.renewalAmount.signum() <= 0) {
throw InvestmentFdService.rule("renew.amount", "The renewal amount must be more than zero");
}
GLAccount diffGl = null;
if (x.topUp.signum() != 0) {
final Long diffGlId = command.longValueOfParameterNamed(DIFF_BANK_GL_ACCOUNT_ID);
if (diffGlId == null) {
throw InvestmentFdService.rule("renew.bank.gl", x.topUp.signum() > 0
? "Choose the bank account the extra " + plain(x.topUp) + " is paid from"
: "Choose the bank account the " + plain(x.topUp.negate()) + " taken out is credited to");
}
diffGl = bankGl(diffGlId, f);
}
final BigDecimal added = x.renewalAmount.subtract(f.getPrincipal());
if (added.signum() > 0) {
final InvestmentFdLimitData limit = this.fdService.limits(date, f.getParty(), added);
if (!limit.isWithinLimits()) {
throw InvestmentFdService.rule("limit.exceeded", limit.getMessage());
}
}
this.interestService.requirePostedBefore(f, date);
final Entry e = renewalEntry(f, x, diffGl);
final CommandProcessingResult posted = e.isEmpty() ? null
: this.journalPoster.post(date, f.getDealNo(), "FD renewed: deal " + f.getDealNo() + ", certificate " + f.getCertNo() + " → "
+ newCertNo + " with " + f.getParty().getPartyName() + " (interest " + plain(x.interest) + tdsNote(x) + ")", e.lines);
final InvestmentFd n = new InvestmentFd();
n.setDealNo(this.fdService.nextDealNo(date));
n.setParty(f.getParty());
n.setFdGlAccount(f.getParty().getGlAccount());
n.setInterestGlAccount(f.getParty().getInterestGlAccount() != null ? f.getParty().getInterestGlAccount() : f.getInterestGlAccount());
n.setCertNo(newCertNo);
n.setCertDate(command.localDateValueOfParameterNamed(NEW_CERT_DATE));
n.setDealDate(date);
n.setPrincipal(x.renewalAmount);
n.setIntRate(command.bigDecimalValueOfParameterNamed(NEW_INT_RATE));
n.setMaturityDate(newMaturity);
n.setPayout(StringUtils.defaultIfBlank(command.stringValueOfParameterNamed(NEW_PAYOUT), f.getPayout()));
n.setCalcType(StringUtils.defaultIfBlank(command.stringValueOfParameterNamed(NEW_CALC_TYPE), f.getCalcType()));
final BigDecimal newInterest = InvestmentFdCalculator.interest(n.getPrincipal(), n.getIntRate(), date, newMaturity, n.getCalcType(),
n.getPayout());
n.setExpectedInterest(newInterest);
n.setExpectedMaturityAmount(InvestmentFdService.maturityAmount(n.getPrincipal(), newInterest, n.getPayout()));
n.setBankGlAccount(diffGl != null ? diffGl : f.getBankGlAccount());
n.setRenewedFrom(f);
n.setStatus(STATUS_ACTIVE);
n.setRemarks(StringUtils.trimToNull(command.stringValueOfParameterNamed(REMARKS)));
n.setGlTransactionId(posted == null ? null : posted.getTransactionId());
n.setVoucherNo(posted == null ? null : voucherNo(posted));
closeRecord(f, x, CLOSURE_RENEWAL, date, STATUS_RENEWED, posted);
this.fdRepository.saveAndFlush(f);
final InvestmentFd saved = this.fdRepository.saveAndFlush(n);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(saved.getId()).withSubEntityId(f.getId())
.withResourceIdAsString(saved.getVoucherNo()).with(Map.<String, Object>of("dealNo", saved.getDealNo(), "voucherNo",
String.valueOf(saved.getVoucherNo()), "interest", x.interest))
.build();
}
// -------------------------------------------------------------------------------------------------- shared
private CommandProcessingResult close(final InvestmentFd f, final JsonCommand command, final String type, final LocalDate date,
final String status) {
this.interestService.requirePostedBefore(f, date);
final Figures x = figures(f, type, date, amount(command, RECEIVED_AMOUNT), amount(command, TDS),
null).withPenal(amount(command, PENAL_AMOUNT));
if (x.received.compareTo(f.getPrincipal()) < 0) {
throw InvestmentFdService.rule("received.below.principal", "The amount received " + plain(x.received)
+ " is less than the principal " + plain(f.getPrincipal()) + "; check the figure");
}
final GLAccount principalGl = command.longValueOfParameterNamed(PRINCIPAL_BANK_GL_ACCOUNT_ID) == null ? f.getBankGlAccount()
: bankGl(command.longValueOfParameterNamed(PRINCIPAL_BANK_GL_ACCOUNT_ID), f);
final GLAccount interestGl = command.longValueOfParameterNamed(INTEREST_BANK_GL_ACCOUNT_ID) == null ? principalGl
: bankGl(command.longValueOfParameterNamed(INTEREST_BANK_GL_ACCOUNT_ID), f);
final Entry e = closureEntry(f, x, principalGl, interestGl);
final String what = CLOSURE_PREMATURE.equals(type) ? "FD closed premature" : "FD matured";
final CommandProcessingResult posted = this.journalPoster.post(date, f.getDealNo(), what + ": deal " + f.getDealNo() + ", certificate "
+ f.getCertNo() + " with " + f.getParty().getPartyName() + " (interest " + plain(x.interest) + tdsNote(x) + ")", e.lines);
f.setRemarks(StringUtils.defaultIfBlank(StringUtils.trimToNull(command.stringValueOfParameterNamed(REMARKS)), f.getRemarks()));
closeRecord(f, x, type, date, status, posted);
this.fdRepository.saveAndFlush(f);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(f.getId())
.withResourceIdAsString(f.getClosureVoucherNo()).withTransactionId(posted.getTransactionId())
.with(Map.<String, Object>of("voucherNo", String.valueOf(f.getClosureVoucherNo()), "interest", x.interest)).build();
}
/** Dr bank (principal, and interest net of TDS), Dr TDS receivable / Cr FD GL, Cr interest receivable, ± interest income. */
private Entry closureEntry(final InvestmentFd f, final Figures x, final GLAccount principalGl, final GLAccount interestGl) {
final Entry e = new Entry();
e.dr(principalGl, f.getPrincipal());
e.dr(interestGl, x.received.subtract(f.getPrincipal()));
e.dr(gl(TDS_RECEIVABLE_GL_CODE), x.tds);
e.cr(f.getFdGlAccount(), f.getPrincipal());
income(e, f, x);
return e;
}
/** Interest income and receivable, plus the FD GL moving from the old principal to the renewal amount and the bank for any difference. */
private Entry renewalEntry(final InvestmentFd f, final Figures x, final GLAccount diffGl) {
final Entry e = new Entry();
e.dr(gl(TDS_RECEIVABLE_GL_CODE), x.tds);
income(e, f, x);
final BigDecimal fdChange = x.renewalAmount.subtract(f.getPrincipal());
if (fdChange.signum() > 0) {
e.dr(f.getFdGlAccount(), fdChange);
} else {
e.cr(f.getFdGlAccount(), fdChange.negate());
}
if (diffGl != null) {
if (x.topUp.signum() > 0) {
e.cr(diffGl, x.topUp);
} else {
e.dr(diffGl, x.topUp.negate());
}
}
return e;
}
private void income(final Entry e, final InvestmentFd f, final Figures x) {
e.cr(gl(INTEREST_RECEIVABLE_GL_CODE), x.provisioned);
if (x.income.signum() > 0) {
e.cr(f.getInterestGlAccount(), x.income);
} else {
// Less interest than already provisioned: the shortfall comes back out of income.
e.dr(f.getInterestGlAccount(), x.income.negate());
}
}
private Figures figures(final InvestmentFd f, final String type, final LocalDate date, final BigDecimal amountIn, final BigDecimal tdsIn,
final BigDecimal renewalIn) {
final List<InvestmentFdInterest> lines = this.interestRepository.findByFdIdOrderByIntDateAscIdAsc(f.getId());
BigDecimal provisioned = BigDecimal.ZERO;
LocalDate lastPaid = f.getDealDate();
for (final InvestmentFdInterest l : lines) {
if (LINE_PROVISION.equals(l.getLineType())) {
provisioned = provisioned.add(l.getAmount());
} else if (LINE_PAYOUT.equals(l.getLineType())) {
provisioned = provisioned.subtract(zero(l.getAlreadyBooked()));
lastPaid = l.getIntDate();
}
}
final BigDecimal expected = InvestmentFdCalculator.interest(f.getPrincipal(), f.getIntRate(), lastPaid, date, f.getCalcType(),
f.getPayout());
final BigDecimal tds = scale(zero(tdsIn));
// Maturity / renewal default to the system's figure; a premature closure always needs the bank's amount.
final BigDecimal received = amountIn != null ? scale(amountIn) : f.getPrincipal().add(expected).subtract(tds);
final BigDecimal interest = received.add(tds).subtract(f.getPrincipal());
final BigDecimal renewal = renewalIn != null ? scale(renewalIn) : received;
return new Figures(expected, provisioned.max(BigDecimal.ZERO), received, tds, null, interest,
interest.subtract(provisioned.max(BigDecimal.ZERO)), renewal, renewal.subtract(received));
}
private void closeRecord(final InvestmentFd f, final Figures x, final String type, final LocalDate date, final String status,
final CommandProcessingResult posted) {
f.setStatus(status);
f.setClosureType(type);
f.setClosedOnDate(date);
f.setClosureAmount(x.received);
f.setClosureInterest(x.interest);
f.setClosureTds(x.tds);
f.setClosurePenal(x.penal);
f.setClosureGlTransactionId(posted == null ? null : posted.getTransactionId());
f.setClosureVoucherNo(posted == null ? null : voucherNo(posted));
}
private InvestmentFd active(final Long fdId) {
final InvestmentFd f = this.fdRepository.findByIdWithLock(fdId)
.orElseThrow(() -> InvestmentFdService.rule("not.found", "FD " + fdId + " does not exist"));
if (!STATUS_ACTIVE.equals(f.getStatus())) {
throw InvestmentFdService.rule("not.active", "FD " + f.getDealNo() + " is already " + f.getStatus().toLowerCase());
}
return f;
}
private GLAccount bankGl(final Long glId, final InvestmentFd f) {
final GLAccount gl = this.glAccountRepository.findOneWithNotFoundDetection(glId);
if (gl.isHeaderAccount() || gl.isDisabled() || !GLAccountType.ASSET.getValue().equals(gl.getType())) {
throw InvestmentFdService.rule("bank.gl.invalid", "GL " + gl.getGlCode() + " must be an enabled detail Asset account (our bank account)");
}
if (gl.getId().equals(f.getFdGlAccount().getId())) {
throw InvestmentFdService.rule("bank.gl.same", "Choose our bank account, not the FD GL itself");
}
return gl;
}
private GLAccount gl(final String code) {
return this.glAccountRepository.findOneByGlCodeWithNotFoundDetection(code);
}
private BigDecimal amount(final JsonCommand command, final String param) {
return command.parameterExists(param) ? command.bigDecimalValueOfParameterNamed(param) : null;
}
private void amountOptional(final DataValidatorBuilder v, final JsonElement el, final String param) {
v.reset().parameter(param).value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(param, el)).ignoreIfNull()
.zeroOrPositiveAmount();
}
private void validate(final String json, final Set<String> supported,
final java.util.function.BiConsumer<DataValidatorBuilder, JsonElement> rules) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, supported);
final List<ApiParameterError> errors = new ArrayList<>();
rules.accept(new DataValidatorBuilder(errors).resource("investmentfd"), this.fromApiJsonHelper.parse(json));
if (!errors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors);
}
}
private static String tdsNote(final Figures x) {
return x.tds.signum() > 0 ? ", TDS " + plain(x.tds) : "";
}
private static BigDecimal zero(final BigDecimal v) {
return v == null ? BigDecimal.ZERO : v;
}
private static BigDecimal scale(final BigDecimal v) {
return v.setScale(2, RoundingMode.HALF_UP);
}
private static String plain(final BigDecimal v) {
return v.setScale(2, RoundingMode.HALF_UP).toPlainString();
}
private static String voucherNo(final CommandProcessingResult posted) {
final Object voucherNo = posted.getChanges() == null ? null : posted.getChanges().get("voucherNo");
return voucherNo == null ? null : voucherNo.toString();
}
/** The figures of one closure. */
private record Figures(BigDecimal expected, BigDecimal provisioned, BigDecimal received, BigDecimal tds, BigDecimal penal,
BigDecimal interest, BigDecimal income, BigDecimal renewalAmount, BigDecimal topUp) {
Figures withPenal(final BigDecimal penalAmount) {
return new Figures(expected, provisioned, received, tds, penalAmount, interest, income, renewalAmount, topUp);
}
InvestmentFdClosureData toData(final InvestmentFd f, final String type, final LocalDate date, final BigDecimal penalAmount,
final Entry e) {
return InvestmentFdClosureData.builder().fdId(f.getId()).dealNo(f.getDealNo()).type(type).closeDate(date.format(DATE))
.principal(f.getPrincipal()).expectedInterest(expected).provisioned(provisioned).receivedAmount(received).tds(tds)
.penalAmount(penalAmount).interest(interest).income(income).difference(interest.subtract(expected))
.renewalAmount(CLOSURE_RENEWAL.equals(type) ? renewalAmount : null).topUp(CLOSURE_RENEWAL.equals(type) ? topUp : null)
.lines(e.view).build();
}
}
/** Journal lines and the same lines for the screen. */
private static final class Entry {
private final InvestmentJournalPoster.Lines lines = new InvestmentJournalPoster.Lines();
private final List<InvestmentFdClosureData.Line> view = new ArrayList<>();
void dr(final GLAccount gl, final BigDecimal amount) {
if (amount != null && amount.signum() > 0) {
this.lines.debit(gl.getId(), amount);
this.view.add(new InvestmentFdClosureData.Line("Dr", gl.getGlCode(), gl.getName(), amount));
}
}
void cr(final GLAccount gl, final BigDecimal amount) {
if (amount != null && amount.signum() > 0) {
this.lines.credit(gl.getId(), amount);
this.view.add(new InvestmentFdClosureData.Line("Cr", gl.getGlCode(), gl.getName(), amount));
}
}
boolean isEmpty() {
return this.view.isEmpty();
}
}
}
@@ -0,0 +1,327 @@
/**
* 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.InvestmentFdApiConstants.BANK_GL_ACCOUNT_ID;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.CALC_SIMPLE;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.INTEREST_PARAMS;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.INTEREST_RECEIVABLE_GL_CODE;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.INT_DATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.LINE_MATURITY;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.LINE_PAYOUT;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.LINE_PROVISION;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.LINE_TYPE;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.PAYOUT_AT_MATURITY;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.PAYOUT_HALF_YEARLY;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.RECEIVED_AMOUNT;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.STATUS_ACTIVE;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.TDS;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.TDS_RECEIVABLE_GL_CODE;
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.Month;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
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.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.data.InvestmentFdDueData;
import org.apache.fineract.portfolio.investment.data.InvestmentFdScheduleData;
import org.apache.fineract.portfolio.investment.domain.InvestmentFd;
import org.apache.fineract.portfolio.investment.domain.InvestmentFdInterest;
import org.apache.fineract.portfolio.investment.domain.InvestmentFdInterestRepository;
import org.apache.fineract.portfolio.investment.domain.InvestmentFdRepository;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* Interest of an FD placed with another bank, as for Govt securities: on 31 March and 30 September the interest
* earned since the last payout (or the deal date) is provisioned (Dr Interest Receivable / Cr Interest on FDs); a
* half-yearly / yearly FD pays its interest every six / twelve months (Dr Bank, Dr TDS receivable when deducted / Cr
* Interest Receivable for what was provisioned, Cr Interest on FDs for the rest). Lines are posted in date order, on or
* after their date, dated that date. The interest of the last period comes with the maturity.
*/
@Service
@RequiredArgsConstructor
public class InvestmentFdInterestService {
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-yyyy");
private final FromJsonHelper fromApiJsonHelper;
private final InvestmentFdRepository fdRepository;
private final InvestmentFdInterestRepository interestRepository;
private final GLAccountRepositoryWrapper glAccountRepository;
private final InvestmentJournalPoster journalPoster;
/** One worked-out interest line. */
record Line(String type, LocalDate date, LocalDate from, BigDecimal gross, BigDecimal alreadyBooked, BigDecimal tds, BigDecimal amount,
InvestmentFdInterest posted) {}
@Transactional(readOnly = true)
public InvestmentFdScheduleData schedule(final Long fdId) {
final InvestmentFd f = this.fdRepository.findById(fdId).orElseThrow(() -> InvestmentFdService.rule("not.found", "FD " + fdId + " does not exist"));
final List<Line> lines = lines(f);
final LocalDate today = DateUtils.getBusinessLocalDate();
final Optional<Line> next = lines.stream().filter(l -> l.posted() == null && !LINE_MATURITY.equals(l.type())).findFirst();
final List<InvestmentFdScheduleData.Row> rows = new ArrayList<>();
for (final Line l : lines) {
rows.add(InvestmentFdScheduleData.Row.builder().lineType(l.type()).intDate(l.date().format(DATE)).fromDate(l.from().format(DATE))
.toDate(l.date().format(DATE)).days(ChronoUnit.DAYS.between(l.from(), l.date())).grossInterest(l.gross())
.alreadyBooked(l.alreadyBooked()).tds(l.tds()).amount(l.amount()).posted(l.posted() != null)
.voucherNo(l.posted() == null ? null : l.posted().getVoucherNo())
.glTransactionId(l.posted() == null ? null : l.posted().getGlTransactionId())
.postable(STATUS_ACTIVE.equals(f.getStatus()) && next.isPresent() && next.get() == l && !l.date().isAfter(today)).build());
}
return InvestmentFdScheduleData.builder().fd(InvestmentFdService.data(f)).provisioned(provisioned(f)).rows(rows).build();
}
/** For every active FD: its next interest voucher, or its maturity, when due now or within {@code withinDays}. */
@Transactional(readOnly = true)
public List<InvestmentFdDueData> due(final int withinDays) {
final LocalDate today = DateUtils.getBusinessLocalDate();
final LocalDate upTo = today.plusDays(Math.max(withinDays, 0));
final List<InvestmentFdDueData> due = new ArrayList<>();
for (final InvestmentFd f : this.fdRepository.findByStatusOrderByIdAsc(STATUS_ACTIVE)) {
lines(f).stream().filter(l -> l.posted() == null).findFirst().filter(l -> !l.date().isAfter(upTo))
.ifPresent(l -> due.add(InvestmentFdDueData.builder().fdId(f.getId()).dealNo(f.getDealNo())
.partyName(f.getParty().getPartyName()).certNo(f.getCertNo()).lineType(l.type()).intDate(l.date().format(DATE))
.amount(LINE_MATURITY.equals(l.type()) ? f.getExpectedMaturityAmount() : l.amount())
.postable(!l.date().isAfter(today)).overdue(l.date().isBefore(today)).build()));
}
return due;
}
/** Refuses a closure on {@code date} while an interest line before it is still unposted. */
void requirePostedBefore(final InvestmentFd f, final LocalDate date) {
lines(f).stream().filter(l -> l.posted() == null && !LINE_MATURITY.equals(l.type()) && l.date().isBefore(date)).findFirst()
.ifPresent(l -> {
throw InvestmentFdService.rule("interest.pending", "Post the " + l.type().toLowerCase() + " of "
+ l.date().format(DATE) + " for FD " + f.getDealNo() + " first (FD Interest Entry)");
});
}
@Transactional
public CommandProcessingResult post(final Long fdId, final JsonCommand command) {
validate(command.json());
final InvestmentFd f = this.fdRepository.findByIdWithLock(fdId)
.orElseThrow(() -> InvestmentFdService.rule("not.found", "FD " + fdId + " does not exist"));
if (!STATUS_ACTIVE.equals(f.getStatus())) {
throw InvestmentFdService.rule("not.active", "FD " + f.getDealNo() + " is already " + f.getStatus().toLowerCase());
}
final String type = command.stringValueOfParameterNamed(LINE_TYPE);
final LocalDate date = command.localDateValueOfParameterNamed(INT_DATE);
if (date.isAfter(DateUtils.getBusinessLocalDate())) {
throw InvestmentFdService.rule("before.date", "This interest line can be posted on or after " + date.format(DATE));
}
final Line next = lines(f).stream().filter(l -> l.posted() == null && !LINE_MATURITY.equals(l.type())).findFirst()
.orElseThrow(() -> InvestmentFdService.rule("all.posted", "FD " + f.getDealNo() + " has no interest line left to post; close it at maturity"));
if (!next.type().equals(type) || !next.date().equals(date)) {
throw InvestmentFdService.rule("out.of.order", "Post the interest lines in date order: next for FD " + f.getDealNo() + " is the "
+ next.type().toLowerCase() + " of " + next.date().format(DATE));
}
final GLAccount receivable = gl(INTEREST_RECEIVABLE_GL_CODE);
final InvestmentJournalPoster.Lines lines = new InvestmentJournalPoster.Lines();
final InvestmentFdInterest x = new InvestmentFdInterest();
x.setFd(f);
x.setLineType(type);
x.setIntDate(date);
x.setFromDate(next.from());
x.setToDate(date);
x.setDays((int) ChronoUnit.DAYS.between(next.from(), date));
final String what;
if (LINE_PROVISION.equals(type)) {
if (next.amount().signum() <= 0) {
throw InvestmentFdService.rule("nothing.to.provision", "No interest to provision on " + date.format(DATE));
}
lines.debit(receivable.getId(), next.amount()).credit(f.getInterestGlAccount().getId(), next.amount());
x.setGrossInterest(next.gross());
x.setAmount(next.amount());
what = "FD interest provision";
} else {
// The bank's own figure when given (net of TDS), else the system's interest for the period.
final BigDecimal tds = amount(command, TDS);
final BigDecimal received = command.parameterExists(RECEIVED_AMOUNT) && command.bigDecimalValueOfParameterNamed(RECEIVED_AMOUNT) != null
? amount(command, RECEIVED_AMOUNT)
: next.gross().subtract(tds);
final BigDecimal gross = received.add(tds);
final BigDecimal income = gross.subtract(next.alreadyBooked());
final GLAccount bank = command.longValueOfParameterNamed(BANK_GL_ACCOUNT_ID) == null ? f.getBankGlAccount()
: bankGl(command.longValueOfParameterNamed(BANK_GL_ACCOUNT_ID), f);
lines.debit(bank.getId(), received).debit(gl(TDS_RECEIVABLE_GL_CODE).getId(), tds).credit(receivable.getId(), next.alreadyBooked());
if (income.signum() >= 0) {
lines.credit(f.getInterestGlAccount().getId(), income);
} else {
lines.debit(f.getInterestGlAccount().getId(), income.negate());
}
x.setGrossInterest(gross);
x.setAlreadyBooked(next.alreadyBooked());
x.setTds(tds);
x.setAmount(income);
x.setBankGlAccount(bank);
what = "FD interest received";
}
final CommandProcessingResult posted = this.journalPoster.post(date, f.getDealNo(), what + " " + date.format(DATE) + ": deal "
+ f.getDealNo() + ", certificate " + f.getCertNo() + " with " + f.getParty().getPartyName(), lines);
x.setGlTransactionId(posted.getTransactionId());
final Object voucherNo = posted.getChanges() == null ? null : posted.getChanges().get("voucherNo");
x.setVoucherNo(voucherNo == null ? null : voucherNo.toString());
x.setPostedOnDate(DateUtils.getBusinessLocalDate());
final InvestmentFdInterest saved = this.interestRepository.saveAndFlush(x);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(saved.getId()).withSubEntityId(f.getId())
.withResourceIdAsString(saved.getVoucherNo()).withTransactionId(posted.getTransactionId()).build();
}
// -------------------------------------------------------------------------------------------------- schedule
/** Every interest line of the FD in date order, with the posted figures where posted and the worked-out ones otherwise. */
List<Line> lines(final InvestmentFd f) {
final List<InvestmentFdInterest> posted = this.interestRepository.findByFdIdOrderByIntDateAscIdAsc(f.getId());
final LocalDate end = f.getClosedOnDate() != null ? f.getClosedOnDate() : f.getMaturityDate();
final List<Object[]> events = new ArrayList<>();
if (!PAYOUT_AT_MATURITY.equals(f.getPayout())) {
final int months = PAYOUT_HALF_YEARLY.equals(f.getPayout()) ? 6 : 12;
for (int k = 1;; k++) {
final LocalDate d = f.getDealDate().plusMonths((long) months * k);
if (!d.isBefore(f.getMaturityDate())) {
break;
}
events.add(new Object[] { d, LINE_PAYOUT });
}
}
for (int y = f.getDealDate().getYear(); y <= f.getMaturityDate().getYear(); y++) {
for (final LocalDate d : List.of(LocalDate.of(y, Month.MARCH, 31), LocalDate.of(y, Month.SEPTEMBER, 30))) {
if (d.isAfter(f.getDealDate()) && d.isBefore(f.getMaturityDate())) {
events.add(new Object[] { d, LINE_PROVISION });
}
}
}
// A payout falling on a provision date comes first: the provision then starts from the payout.
events.sort(Comparator.<Object[], LocalDate>comparing(e -> (LocalDate) e[0]).thenComparing(e -> LINE_PAYOUT.equals(e[1]) ? 0 : 1));
final List<Line> out = new ArrayList<>();
LocalDate lastPaid = f.getDealDate();
BigDecimal sincePaid = BigDecimal.ZERO;
for (final Object[] e : events) {
final LocalDate d = (LocalDate) e[0];
final String type = (String) e[1];
if (d.isAfter(end)) {
break;
}
final InvestmentFdInterest done = posted.stream().filter(p -> p.getLineType().equals(type) && p.getIntDate().equals(d)).findFirst()
.orElse(null);
if (LINE_PROVISION.equals(type)) {
final BigDecimal accrued = InvestmentFdCalculator.interest(f.getPrincipal(), f.getIntRate(), lastPaid, d, f.getCalcType(),
f.getPayout());
final BigDecimal amount = done != null ? done.getAmount() : accrued.subtract(sincePaid).max(BigDecimal.ZERO);
if (done == null && amount.signum() == 0) {
continue;
}
out.add(new Line(type, d, lastPaid, done != null ? done.getGrossInterest() : accrued, null, null, amount, done));
sincePaid = sincePaid.add(amount);
} else {
final BigDecimal gross = done != null ? done.getGrossInterest()
: InvestmentFdCalculator.interest(f.getPrincipal(), f.getIntRate(), lastPaid, d, CALC_SIMPLE, f.getPayout());
final BigDecimal booked = done != null ? zero(done.getAlreadyBooked()) : sincePaid;
out.add(new Line(type, d, lastPaid, gross, booked, done == null ? null : done.getTds(),
done != null ? done.getAmount() : gross.subtract(booked), done));
lastPaid = d;
sincePaid = BigDecimal.ZERO;
}
}
if (f.getClosedOnDate() == null) {
final BigDecimal last = InvestmentFdCalculator.interest(f.getPrincipal(), f.getIntRate(), lastPaid, f.getMaturityDate(),
f.getCalcType(), f.getPayout());
out.add(new Line(LINE_MATURITY, f.getMaturityDate(), lastPaid, last, sincePaid, null, last.subtract(sincePaid), null));
}
return out;
}
/** Interest provisioned and not yet received. */
BigDecimal provisioned(final InvestmentFd f) {
BigDecimal v = BigDecimal.ZERO;
for (final InvestmentFdInterest l : this.interestRepository.findByFdIdOrderByIntDateAscIdAsc(f.getId())) {
v = LINE_PROVISION.equals(l.getLineType()) ? v.add(l.getAmount()) : v.subtract(zero(l.getAlreadyBooked()));
}
return v.max(BigDecimal.ZERO);
}
private GLAccount bankGl(final Long glId, final InvestmentFd f) {
final GLAccount gl = this.glAccountRepository.findOneWithNotFoundDetection(glId);
if (gl.isHeaderAccount() || gl.isDisabled() || !GLAccountType.ASSET.getValue().equals(gl.getType())
|| gl.getId().equals(f.getFdGlAccount().getId())) {
throw InvestmentFdService.rule("bank.gl.invalid", "GL " + gl.getGlCode() + " must be our bank account (an enabled detail Asset GL)");
}
return gl;
}
private GLAccount gl(final String code) {
return this.glAccountRepository.findOneByGlCodeWithNotFoundDetection(code);
}
private static BigDecimal amount(final JsonCommand command, final String param) {
final BigDecimal v = command.parameterExists(param) ? command.bigDecimalValueOfParameterNamed(param) : null;
return v == null ? BigDecimal.ZERO : v.setScale(2, RoundingMode.HALF_UP);
}
private static BigDecimal zero(final BigDecimal v) {
return v == null ? BigDecimal.ZERO : v;
}
private void validate(final String json) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, INTEREST_PARAMS);
final JsonElement e = this.fromApiJsonHelper.parse(json);
final List<ApiParameterError> errors = new ArrayList<>();
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource("investmentfd");
v.reset().parameter(LINE_TYPE).value(this.fromApiJsonHelper.extractStringNamed(LINE_TYPE, e)).notBlank()
.isOneOfTheseValues(LINE_PROVISION, LINE_PAYOUT);
v.reset().parameter(INT_DATE).value(this.fromApiJsonHelper.extractLocalDateNamed(INT_DATE, e)).notNull();
v.reset().parameter(RECEIVED_AMOUNT).value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(RECEIVED_AMOUNT, e)).ignoreIfNull()
.zeroOrPositiveAmount();
v.reset().parameter(TDS).value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(TDS, e)).ignoreIfNull().zeroOrPositiveAmount();
if (!errors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors);
}
}
}
@@ -0,0 +1,386 @@
/**
* 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.InvestmentFdApiConstants.AMOUNT;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.AS_ON_DATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.BANK_BALANCE_HEADER_GL_CODE;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.BANK_GL_ACCOUNT_ID;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.BANK_LIMIT_PERCENT;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.BASE_PARAMS;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.BOND_TYPE_FD;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.CALC_QUARTERLY;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.CALC_TYPE;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.CALC_TYPES;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.CERT_DATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.CERT_NO;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.DEAL_DATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.DEPOSIT_HEADER_GL_CODE;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.FD_PARAMS;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.GROSS_LIMIT_PERCENT;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.INT_RATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.MATURITY_DATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.PARTY_ID;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.PAYOUT;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.PAYOUTS;
import static org.apache.fineract.portfolio.investment.api.InvestmentFdApiConstants.PAYOUT_AT_MATURITY;
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.TOTAL_DEPOSITS;
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.Month;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
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.data.InvestmentFdData;
import org.apache.fineract.portfolio.investment.data.InvestmentFdLimitData;
import org.apache.fineract.portfolio.investment.domain.InvestmentDepositBase;
import org.apache.fineract.portfolio.investment.domain.InvestmentDepositBaseRepository;
import org.apache.fineract.portfolio.investment.domain.InvestmentFd;
import org.apache.fineract.portfolio.investment.domain.InvestmentFdRepository;
import org.apache.fineract.portfolio.investment.domain.InvestmentParty;
import org.apache.fineract.portfolio.investment.domain.InvestmentPartyRepository;
import org.apache.fineract.portfolio.investment.exception.InvestmentPartyNotFoundException;
import org.springframework.data.domain.Sort;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* FDs the bank places with other banks (Jitendra Sir): the bank is an FD party with its own FD GL and an interest GL;
* placing the money posts Dr FD GL / Cr our bank GL at once, dated the deal date. RBI limits: deposits with other
* banks (current accounts included) within 20% of total deposits as on the previous 31 March, and within 5% with any
* one bank.
*/
@Service
@RequiredArgsConstructor
public class InvestmentFdService {
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-yyyy");
private final FromJsonHelper fromApiJsonHelper;
private final InvestmentFdRepository fdRepository;
private final InvestmentDepositBaseRepository depositBaseRepository;
private final InvestmentPartyRepository partyRepository;
private final GLAccountRepositoryWrapper glAccountRepository;
private final InvestmentJournalPoster journalPoster;
private final JdbcTemplate jdbcTemplate;
// -------------------------------------------------------------------------------------------------- reads
@Transactional(readOnly = true)
public List<InvestmentFdData> retrieveAll() {
return this.fdRepository.findAll(Sort.by(Sort.Direction.DESC, "id")).stream().map(InvestmentFdService::data).toList();
}
@Transactional(readOnly = true)
public InvestmentFdData retrieveOne(final Long fdId) {
return data(this.fdRepository.findById(fdId).orElseThrow(() -> rule("not.found", "FD " + fdId + " does not exist")));
}
/** What an FD would earn and where it stands against the RBI limits, before it is saved. */
@Transactional(readOnly = true)
public InvestmentFdData quote(final Long partyId, final BigDecimal amount, final BigDecimal rate, final LocalDate dealDate,
final LocalDate maturityDate, final String payout, final String calcType) {
// Set.of(...).contains(null) throws, so blank choices fall back to the defaults first.
final String pay = payout != null && PAYOUTS.contains(payout) ? payout : PAYOUT_AT_MATURITY;
final String calc = calcType != null && CALC_TYPES.contains(calcType) ? calcType : CALC_QUARTERLY;
final BigDecimal interest = maturityDate == null || dealDate == null ? BigDecimal.ZERO
: InvestmentFdCalculator.interest(amount, rate, dealDate, maturityDate, calc, pay);
final InvestmentParty party = partyId == null ? null : this.partyRepository.findById(partyId).orElse(null);
return InvestmentFdData.builder().partyId(partyId).partyName(party == null ? null : party.getPartyName())
.principal(amount).intRate(rate).dealDate(format(dealDate)).maturityDate(format(maturityDate))
.days(dealDate == null || maturityDate == null ? 0 : java.time.temporal.ChronoUnit.DAYS.between(dealDate, maturityDate))
.payout(pay).calcType(calc).expectedInterest(interest).expectedMaturityAmount(maturityAmount(amount, interest, pay))
.periodInterest(InvestmentFdCalculator.periodInterest(amount, rate, pay))
.fdGlCode(party == null ? null : party.getGlAccount().getGlCode())
.fdGlName(party == null ? null : party.getGlAccount().getName())
.interestGlCode(party == null || party.getInterestGlAccount() == null ? null : party.getInterestGlAccount().getGlCode())
.interestGlName(party == null || party.getInterestGlAccount() == null ? null : party.getInterestGlAccount().getName())
.limit(limits(dealDate == null ? DateUtils.getBusinessLocalDate() : dealDate, party, amount)).build();
}
/** The RBI limits for an FD of {@code amount} with {@code party} (either may be empty) on {@code dealDate}. */
@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 BigDecimal newAmount = amount == null ? BigDecimal.ZERO : amount;
final BigDecimal fdOutstanding = this.fdRepository.findByStatusOrderByIdAsc(STATUS_ACTIVE).stream().map(InvestmentFd::getPrincipal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
final BigDecimal bankBalances = bankBalances();
final BigDecimal withParty = party == null ? BigDecimal.ZERO
: this.fdRepository.findByStatusOrderByIdAsc(STATUS_ACTIVE).stream().filter(f -> f.getParty().getId().equals(party.getId()))
.map(InvestmentFd::getPrincipal).reduce(BigDecimal.ZERO, BigDecimal::add);
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)
.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)
+ " first; the RBI limits on deposits with other banks are worked out from it").build();
}
final BigDecimal grossLimit = percent(total, GROSS_LIMIT_PERCENT);
final BigDecimal bankLimit = percent(total, BANK_LIMIT_PERCENT);
b.grossLimit(grossLimit).bankLimit(bankLimit);
if (grossAfter.compareTo(grossLimit) > 0) {
return b.withinLimits(false).message("Deposits with other banks would be " + plain(grossAfter) + ", above the RBI limit of "
+ plain(grossLimit) + " (20% of total deposits " + plain(total) + " as on " + base.format(DATE) + ")").build();
}
if (party != null && bankAfter.compareTo(bankLimit) > 0) {
return b.withinLimits(false).message("Deposits with " + party.getPartyName() + " would be " + plain(bankAfter)
+ ", above the RBI limit of " + plain(bankLimit) + " with one bank (5% of total deposits " + plain(total) + ")")
.build();
}
return b.withinLimits(true).message("Within the RBI limits").build();
}
// -------------------------------------------------------------------------------------------------- writes
/** Places an FD: Dr the bank's FD GL / Cr our bank GL, dated the deal date, voucher raised at once. */
@Transactional
public CommandProcessingResult create(final JsonCommand command) {
validateFd(command.json());
final Long partyId = command.longValueOfParameterNamed(PARTY_ID);
final InvestmentParty party = this.partyRepository.findByIdWithLock(partyId)
.orElseThrow(() -> new InvestmentPartyNotFoundException(partyId));
if (!BOND_TYPE_FD.equals(party.getBondType())) {
throw rule("party.not.fd", "Party " + party.getPartyCode() + " is not an FD bank (Bond Type " + party.getBondType() + ")");
}
if (!party.isActive()) {
throw rule("party.inactive", "Party " + party.getPartyCode() + " is inactive");
}
if (party.getInterestGlAccount() == null) {
throw rule("party.no.interest.gl", "Map the interest GL of " + party.getPartyName() + " in Bank / Party Master first");
}
final LocalDate today = DateUtils.getBusinessLocalDate();
final LocalDate dealDate = command.localDateValueOfParameterNamed(DEAL_DATE);
final LocalDate maturityDate = command.localDateValueOfParameterNamed(MATURITY_DATE);
if (dealDate.isAfter(today)) {
throw rule("deal.future", "Deal date cannot be after today");
}
if (!maturityDate.isAfter(dealDate)) {
throw rule("maturity.before.deal", "Maturity date must be after the deal date");
}
final String certNo = command.stringValueOfParameterNamed(CERT_NO).trim();
if (this.fdRepository.existsByPartyIdAndCertNoIgnoreCase(partyId, certNo)) {
throw rule("cert.duplicate", "Certificate " + certNo + " of " + party.getPartyName() + " is already entered");
}
final GLAccount bankGl = postableAsset(command.longValueOfParameterNamed(BANK_GL_ACCOUNT_ID));
if (bankGl.getId().equals(party.getGlAccount().getId())) {
throw rule("bank.gl.same", "The money cannot be paid from the FD GL itself; choose our bank account");
}
final BigDecimal amount = command.bigDecimalValueOfParameterNamed(AMOUNT).setScale(2, RoundingMode.HALF_UP);
final InvestmentFdLimitData limit = limits(dealDate, party, amount);
if (!limit.isWithinLimits()) {
throw rule("limit.exceeded", limit.getMessage());
}
final InvestmentFd f = new InvestmentFd();
f.setDealNo(nextDealNo(dealDate));
f.setParty(party);
f.setFdGlAccount(party.getGlAccount());
f.setInterestGlAccount(party.getInterestGlAccount());
f.setCertNo(certNo);
f.setCertDate(command.localDateValueOfParameterNamed(CERT_DATE));
f.setDealDate(dealDate);
f.setPrincipal(amount);
f.setIntRate(command.bigDecimalValueOfParameterNamed(INT_RATE));
f.setMaturityDate(maturityDate);
f.setPayout(command.stringValueOfParameterNamed(PAYOUT));
f.setCalcType(StringUtils.defaultIfBlank(command.stringValueOfParameterNamed(CALC_TYPE), CALC_QUARTERLY));
final BigDecimal interest = InvestmentFdCalculator.interest(amount, f.getIntRate(), dealDate, maturityDate, f.getCalcType(),
f.getPayout());
f.setExpectedInterest(interest);
f.setExpectedMaturityAmount(maturityAmount(amount, interest, f.getPayout()));
f.setBankGlAccount(bankGl);
f.setRemarks(StringUtils.trimToNull(command.stringValueOfParameterNamed(REMARKS)));
f.setStatus(STATUS_ACTIVE);
final CommandProcessingResult posted = this.journalPoster.post(dealDate, f.getDealNo(),
"FD placed: deal " + f.getDealNo() + " with " + party.getPartyName() + ", certificate " + certNo + ", "
+ f.getIntRate().stripTrailingZeros().toPlainString() + "% to " + maturityDate.format(DATE),
new InvestmentJournalPoster.Lines().debit(party.getGlAccount().getId(), amount).credit(bankGl.getId(), amount));
f.setGlTransactionId(posted.getTransactionId());
f.setVoucherNo(voucherNo(posted));
final InvestmentFd saved = this.fdRepository.saveAndFlush(f);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(saved.getId())
.withResourceIdAsString(saved.getVoucherNo()).withTransactionId(posted.getTransactionId())
.with(Map.<String, Object>of("dealNo", saved.getDealNo(), "voucherNo", String.valueOf(saved.getVoucherNo()))).build();
}
/** Records the bank's total deposits as on a 31 March, the base of the RBI limits for the following year. */
@Transactional
public CommandProcessingResult setDepositBase(final JsonCommand command) {
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()
.positiveAmount();
v.reset().parameter(REMARKS).value(this.fromApiJsonHelper.extractStringNamed(REMARKS, e)).ignoreIfNull().notExceedingLengthOf(500);
});
final LocalDate asOn = command.localDateValueOfParameterNamed(AS_ON_DATE);
if (asOn.getMonth() != Month.MARCH || asOn.getDayOfMonth() != 31) {
throw rule("base.not.yearend", "Total deposits are taken as on 31 March");
}
final InvestmentDepositBase base = this.depositBaseRepository.findByAsOnDate(asOn).orElseGet(InvestmentDepositBase::new);
base.setAsOnDate(asOn);
base.setTotalDeposits(command.bigDecimalValueOfParameterNamed(TOTAL_DEPOSITS));
base.setRemarks(StringUtils.trimToNull(command.stringValueOfParameterNamed(REMARKS)));
final InvestmentDepositBase saved = this.depositBaseRepository.saveAndFlush(base);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(saved.getId())
.withResourceIdAsString(asOn.format(DATE)).build();
}
// -------------------------------------------------------------------------------------------------- helpers
static InvestmentFdData data(final InvestmentFd f) {
return InvestmentFdData.from(f, InvestmentFdCalculator.periodInterest(f.getPrincipal(), f.getIntRate(), f.getPayout()));
}
static BigDecimal maturityAmount(final BigDecimal amount, final BigDecimal interest, final String payout) {
if (amount == null) {
return null;
}
// A payout FD returns only the principal at maturity (with the last period's interest, posted as a payout).
return PAYOUT_AT_MATURITY.equals(payout) ? amount.add(interest) : amount;
}
/** The 31 March of the previous financial year: for 16-10-2026 → 31-03-2026, for 31-03-2026 → 31-03-2025. */
static LocalDate previousYearEnd(final LocalDate date) {
final int year = date.getMonthValue() >= 4 ? date.getYear() : date.getYear() - 1;
return LocalDate.of(year, Month.MARCH, 31);
}
/** Balances in current accounts with banks today (debit balances only), part of the inter-bank exposure. */
private BigDecimal bankBalances() {
final BigDecimal v = this.jdbcTemplate.queryForObject("""
SELECT COALESCE(SUM(GREATEST(t.bal, 0)), 0) FROM (
SELECT SUM(CASE WHEN je.type_enum = 2 THEN je.amount ELSE -je.amount END) AS bal
FROM acc_gl_journal_entry je JOIN acc_gl_account a ON a.id = je.account_id
WHERE a.parent_id = (SELECT id FROM acc_gl_account WHERE gl_code = ?) AND je.reversed = false
GROUP BY je.account_id) t""", BigDecimal.class, BANK_BALANCE_HEADER_GL_CODE);
return v == null ? BigDecimal.ZERO : v.setScale(2, RoundingMode.HALF_UP);
}
/** Total deposits on a date from the deposit GLs (credit balances), shown as a suggestion for the yearly figure. */
private BigDecimal glDeposits(final LocalDate asOn) {
final BigDecimal v = this.jdbcTemplate.queryForObject("""
SELECT COALESCE(SUM(CASE WHEN je.type_enum = 1 THEN je.amount ELSE -je.amount END), 0)
FROM acc_gl_journal_entry je JOIN acc_gl_account a ON a.id = je.account_id
WHERE (a.parent_id = (SELECT id FROM acc_gl_account WHERE gl_code = ?) OR a.gl_code = ?)
AND je.reversed = false AND je.entry_date <= ?""", BigDecimal.class, DEPOSIT_HEADER_GL_CODE, RD_DEPOSIT_GL_CODE, asOn);
return v == null ? BigDecimal.ZERO : v.setScale(2, RoundingMode.HALF_UP);
}
String nextDealNo(final LocalDate dealDate) {
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,
prefix + "%");
return prefix + String.format("%05d", (max == null ? 0 : max) + 1);
}
private GLAccount postableAsset(final Long glAccountId) {
final GLAccount gl = this.glAccountRepository.findOneWithNotFoundDetection(glAccountId);
if (gl.isHeaderAccount() || gl.isDisabled() || !GLAccountType.ASSET.getValue().equals(gl.getType())) {
throw rule("bank.gl.invalid", "GL " + gl.getGlCode() + " must be an enabled detail Asset account (our bank account)");
}
return gl;
}
private void validateFd(final String json) {
validate(json, FD_PARAMS, v -> {
final JsonElement e = this.fromApiJsonHelper.parse(json);
v.reset().parameter(PARTY_ID).value(this.fromApiJsonHelper.extractLongNamed(PARTY_ID, e)).notNull().longGreaterThanZero();
v.reset().parameter(CERT_NO).value(this.fromApiJsonHelper.extractStringNamed(CERT_NO, e)).notBlank().notExceedingLengthOf(50);
v.reset().parameter(DEAL_DATE).value(this.fromApiJsonHelper.extractLocalDateNamed(DEAL_DATE, e)).notNull();
v.reset().parameter(MATURITY_DATE).value(this.fromApiJsonHelper.extractLocalDateNamed(MATURITY_DATE, e)).notNull();
v.reset().parameter(AMOUNT).value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(AMOUNT, e)).notNull().positiveAmount();
v.reset().parameter(INT_RATE).value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(INT_RATE, e)).notNull()
.positiveAmount();
v.reset().parameter(PAYOUT).value(this.fromApiJsonHelper.extractStringNamed(PAYOUT, e)).notBlank()
.isOneOfTheseValues(PAYOUTS.toArray());
v.reset().parameter(CALC_TYPE).value(this.fromApiJsonHelper.extractStringNamed(CALC_TYPE, e)).ignoreIfNull()
.isOneOfTheseValues(CALC_TYPES.toArray());
v.reset().parameter(BANK_GL_ACCOUNT_ID).value(this.fromApiJsonHelper.extractLongNamed(BANK_GL_ACCOUNT_ID, e)).notNull()
.longGreaterThanZero();
v.reset().parameter(REMARKS).value(this.fromApiJsonHelper.extractStringNamed(REMARKS, e)).ignoreIfNull().notExceedingLengthOf(500);
});
}
private void validate(final String json, final Set<String> supported, final java.util.function.Consumer<DataValidatorBuilder> rules) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, supported);
final List<ApiParameterError> errors = new ArrayList<>();
rules.accept(new DataValidatorBuilder(errors).resource("investmentfd"));
if (!errors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors);
}
}
private static BigDecimal percent(final BigDecimal total, final BigDecimal pct) {
return total.multiply(pct).divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
}
private static String plain(final BigDecimal v) {
return v.setScale(2, RoundingMode.HALF_UP).toPlainString();
}
private static String format(final LocalDate date) {
return date == null ? null : date.format(DATE);
}
private static String voucherNo(final CommandProcessingResult posted) {
final Object voucherNo = posted.getChanges() == null ? null : posted.getChanges().get("voucherNo");
return voucherNo == null ? null : voucherNo.toString();
}
static GeneralPlatformDomainRuleException rule(final String code, final String message) {
return new GeneralPlatformDomainRuleException("error.msg.investmentfd." + code, message);
}
}
@@ -35,6 +35,7 @@ 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.INTEREST_GL_ACCOUNT_ID;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.INT_RATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ISIN;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ISSUE_DATE;
@@ -155,6 +156,20 @@ public class InvestmentMasterWritePlatformServiceImpl implements InvestmentMaste
if (command.parameterExists(GL_ACCOUNT_ID)) {
p.setGlAccount(postableAssetGl(command.longValueOfParameterNamed(GL_ACCOUNT_ID)));
}
if (command.parameterExists(INTEREST_GL_ACCOUNT_ID)) {
final Long interestGlId = command.longValueOfParameterNamed(INTEREST_GL_ACCOUNT_ID);
p.setInterestGlAccount(interestGlId == null ? null : postableIncomeGl(interestGlId));
}
}
/** Interest on an FD with this bank is credited here, so it must be a usable Income account. */
private GLAccount postableIncomeGl(final Long glAccountId) {
final GLAccount gl = this.glAccountRepository.findOneWithNotFoundDetection(glAccountId);
if (gl.isHeaderAccount() || gl.isDisabled() || !GLAccountType.INCOME.getValue().equals(gl.getType())) {
throw new GeneralPlatformDomainRuleException("error.msg.investment.glaccount.not.income",
"GL " + gl.getGlCode() + " must be an enabled detail Income account", glAccountId);
}
return gl;
}
private InvestmentParty saveParty(final InvestmentParty party) {
@@ -183,4 +183,5 @@
<include file="parts/0601_investment_sale_redemption.xml" relativeToChangelogFile="true"/>
<include file="parts/0602_investment_security_transfer.xml" relativeToChangelogFile="true"/>
<include file="parts/0603_investment_premium_amortisation.xml" relativeToChangelogFile="true"/>
<include file="parts/0604_investment_fd.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
@@ -0,0 +1,217 @@
<?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">
<!--
Fixed deposits the bank places with other banks (investment, not customer FDs). Each bank is an FD party with its
own FD GL and an interest GL. The RBI limits on deposits with other banks (20% of total deposits as on the previous
31 March, 5% with one bank) are checked against the yearly total-deposits figure kept here.
-->
<changeSet author="cbs-anshul" id="0604-1-investment-party-interest-gl">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_investment_party" columnName="interest_gl_account_id"/></not>
</preConditions>
<addColumn tableName="m_investment_party">
<column name="interest_gl_account_id" type="BIGINT"/>
</addColumn>
<addForeignKeyConstraint baseTableName="m_investment_party" baseColumnNames="interest_gl_account_id"
constraintName="fk_investment_party_interest_gl" referencedTableName="acc_gl_account" referencedColumnNames="id"/>
</changeSet>
<changeSet author="cbs-anshul" id="0604-2-create-investment-fd">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_investment_fd"/></not>
</preConditions>
<createTable tableName="m_investment_fd">
<column name="id" type="BIGINT" autoIncrement="true"><constraints nullable="false" primaryKey="true"/></column>
<column name="deal_no" type="VARCHAR(20)"><constraints nullable="false" unique="true" uniqueConstraintName="uq_investment_fd_deal_no"/></column>
<column name="party_id" type="BIGINT"><constraints nullable="false"/></column>
<column name="fd_gl_account_id" type="BIGINT"><constraints nullable="false"/></column>
<column name="interest_gl_account_id" type="BIGINT"><constraints nullable="false"/></column>
<column name="cert_no" type="VARCHAR(50)"><constraints nullable="false"/></column>
<column name="cert_date" type="DATE"/>
<column name="deal_date" type="DATE"><constraints nullable="false"/></column>
<column name="principal" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
<column name="int_rate" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
<column name="maturity_date" type="DATE"><constraints nullable="false"/></column>
<column name="payout" type="VARCHAR(20)"><constraints nullable="false"/></column>
<column name="calc_type" type="VARCHAR(20)"><constraints nullable="false"/></column>
<column name="expected_interest" type="DECIMAL(19,6)"/>
<column name="expected_maturity_amount" type="DECIMAL(19,6)"/>
<column name="bank_gl_account_id" type="BIGINT"><constraints nullable="false"/></column>
<column name="renewed_from_id" type="BIGINT"/>
<column name="status" type="VARCHAR(20)"><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="closed_on_date" type="DATE"/>
<column name="closure_type" type="VARCHAR(20)"/>
<column name="closure_amount" type="DECIMAL(19,6)"/>
<column name="closure_interest" type="DECIMAL(19,6)"/>
<column name="closure_tds" type="DECIMAL(19,6)"/>
<column name="closure_penal" type="DECIMAL(19,6)"/>
<column name="closure_gl_transaction_id" type="VARCHAR(50)"/>
<column name="closure_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_fd" baseColumnNames="party_id" constraintName="fk_investment_fd_party"
referencedTableName="m_investment_party" referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="m_investment_fd" baseColumnNames="fd_gl_account_id" constraintName="fk_investment_fd_fd_gl"
referencedTableName="acc_gl_account" referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="m_investment_fd" baseColumnNames="interest_gl_account_id" constraintName="fk_investment_fd_interest_gl"
referencedTableName="acc_gl_account" referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="m_investment_fd" baseColumnNames="bank_gl_account_id" constraintName="fk_investment_fd_bank_gl"
referencedTableName="acc_gl_account" referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="m_investment_fd" baseColumnNames="renewed_from_id" constraintName="fk_investment_fd_renewed_from"
referencedTableName="m_investment_fd" referencedColumnNames="id"/>
</changeSet>
<changeSet author="cbs-anshul" id="0604-3-create-investment-deposit-base">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_investment_deposit_base"/></not>
</preConditions>
<createTable tableName="m_investment_deposit_base">
<column name="id" type="BIGINT" autoIncrement="true"><constraints nullable="false" primaryKey="true"/></column>
<column name="as_on_date" type="DATE"><constraints nullable="false" unique="true" uniqueConstraintName="uq_investment_deposit_base_date"/></column>
<column name="total_deposits" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
<column name="remarks" type="VARCHAR(500)"/>
<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>
</changeSet>
<changeSet author="cbs-anshul" id="0604-4-investment-fd-permissions">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE entity_name = 'INVESTMENTFD'</sqlCheck>
</preConditions>
<sql>
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) VALUES
('portfolio', 'READ_INVESTMENTFD', 'INVESTMENTFD', 'READ', FALSE),
('portfolio', 'CREATE_INVESTMENTFD', 'INVESTMENTFD', 'CREATE', TRUE),
('portfolio', 'CREATE_INVESTMENTFD_CHECKER', 'INVESTMENTFD', 'CREATE_CHECKER', FALSE),
('portfolio', 'SETBASE_INVESTMENTFD', 'INVESTMENTFD', 'SETBASE', FALSE),
('portfolio', 'SETBASE_INVESTMENTFD_CHECKER', 'INVESTMENTFD', 'SETBASE_CHECKER', FALSE)
</sql>
</changeSet>
<changeSet author="cbs-anshul" id="0604-5-grant-investment-fd-permissions">
<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 IN ('READ_INVESTMENTFD', 'CREATE_INVESTMENTFD', 'SETBASE_INVESTMENTFD')
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>
<!-- Interest of an FD: 31 Mar / 30 Sep provisions and half-yearly / yearly payouts; closures clear what is provisioned. -->
<changeSet author="cbs-anshul" id="0604-6-create-investment-fd-interest">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_investment_fd_interest"/></not>
</preConditions>
<createTable tableName="m_investment_fd_interest">
<column name="id" type="BIGINT" autoIncrement="true"><constraints nullable="false" primaryKey="true"/></column>
<column name="fd_id" type="BIGINT"><constraints nullable="false"/></column>
<column name="line_type" type="VARCHAR(20)"><constraints nullable="false"/></column>
<column name="int_date" type="DATE"><constraints nullable="false"/></column>
<column name="from_date" type="DATE"/>
<column name="to_date" type="DATE"/>
<column name="days" type="INT"/>
<column name="gross_interest" type="DECIMAL(19,6)"/>
<column name="already_booked" type="DECIMAL(19,6)"/>
<column name="tds" type="DECIMAL(19,6)"/>
<column name="amount" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
<column name="bank_gl_account_id" type="BIGINT"/>
<column name="gl_transaction_id" type="VARCHAR(50)"/>
<column name="voucher_no" type="VARCHAR(30)"/>
<column name="posted_on_date" type="DATE"/>
<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_fd_interest" baseColumnNames="fd_id" constraintName="fk_investment_fd_interest_fd"
referencedTableName="m_investment_fd" referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="m_investment_fd_interest" baseColumnNames="bank_gl_account_id"
constraintName="fk_investment_fd_interest_bank_gl" referencedTableName="acc_gl_account" referencedColumnNames="id"/>
</changeSet>
<changeSet author="cbs-anshul" id="0604-7-investment-fd-closure-permissions">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'MATURE_INVESTMENTFD'</sqlCheck>
</preConditions>
<sql>
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) VALUES
('portfolio', 'MATURE_INVESTMENTFD', 'INVESTMENTFD', 'MATURE', TRUE),
('portfolio', 'MATURE_INVESTMENTFD_CHECKER', 'INVESTMENTFD', 'MATURE_CHECKER', FALSE),
('portfolio', 'PREMATURE_INVESTMENTFD', 'INVESTMENTFD', 'PREMATURE', TRUE),
('portfolio', 'PREMATURE_INVESTMENTFD_CHECKER', 'INVESTMENTFD', 'PREMATURE_CHECKER', FALSE),
('portfolio', 'RENEW_INVESTMENTFD', 'INVESTMENTFD', 'RENEW', TRUE),
('portfolio', 'RENEW_INVESTMENTFD_CHECKER', 'INVESTMENTFD', 'RENEW_CHECKER', FALSE)
</sql>
</changeSet>
<changeSet author="cbs-anshul" id="0604-8-grant-investment-fd-closure-permissions">
<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 IN ('MATURE_INVESTMENTFD', 'PREMATURE_INVESTMENTFD', 'RENEW_INVESTMENTFD')
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>
<changeSet author="cbs-anshul" id="0604-9-investment-fd-interest-permission">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'POSTINTEREST_INVESTMENTFD'</sqlCheck>
</preConditions>
<sql>
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) VALUES
('portfolio', 'POSTINTEREST_INVESTMENTFD', 'INVESTMENTFD', 'POSTINTEREST', FALSE),
('portfolio', 'POSTINTEREST_INVESTMENTFD_CHECKER', 'INVESTMENTFD', 'POSTINTEREST_CHECKER', FALSE)
</sql>
</changeSet>
<changeSet author="cbs-anshul" id="0604-10-grant-investment-fd-interest-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 = 'POSTINTEREST_INVESTMENTFD'
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>