feat(investment): Bank/Party Master, Script Master and Purchase Entry backend

Treasury Investment module, first slice, following the bank's existing CBS
Investment screens:

- Bank / Party Master (m_investment_party): auto party code PTY-nnn, address,
  bank type, GL A/c, RBI code, exposure limit, LEI, counterparty bank details.
- Script Master (m_investment_script): auto script code by bond type
  (G00001, B00001, ...), SLR type (GOI / STATE_GOVT / TREASURY / NON_SLR),
  GL A/c, ISIN, interest rate, issue / maturity dates, issuer details.
- Purchase Entry (m_investment_purchase): one deal = one holding. FY deal
  number (e.g. 202627000001), auto cert no, settlement date from T+n.
  Units, book value, premium/discount, accrued interest (30/360 up to the
  deal date), settlement amount and YTM are computed by the backend.

APIs: /v1/investment/parties, /v1/investment/scripts, /v1/investment/purchases,
all through the command pipeline. Permissions READ/CREATE/UPDATE/DELETE plus
_CHECKER rows are seeded with maker-checker off and granted to roles that
hold MENU_INVESTMENT. Mapped GLs must be enabled ASSET detail accounts.
No GL entry is posted by a purchase yet.

Migrations: 0593_investment_masters, 0594_investment_purchase.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 12:35:43 +05:30
co-authored by Claude Opus 5.5
parent 88157ee9d6
commit 81b5850b62
37 changed files with 3058 additions and 0 deletions
@@ -95,6 +95,62 @@ public class CommandWrapperBuilder {
return this;
}
public CommandWrapperBuilder createInvestmentParty() {
this.actionName = "CREATE";
this.entityName = "INVESTMENTPARTY";
this.entityId = null;
this.href = "/investment/parties";
return this;
}
public CommandWrapperBuilder updateInvestmentParty(final Long partyId) {
this.actionName = "UPDATE";
this.entityName = "INVESTMENTPARTY";
this.entityId = partyId;
this.href = "/investment/parties/" + partyId;
return this;
}
public CommandWrapperBuilder deleteInvestmentParty(final Long partyId) {
this.actionName = "DELETE";
this.entityName = "INVESTMENTPARTY";
this.entityId = partyId;
this.href = "/investment/parties/" + partyId;
return this;
}
public CommandWrapperBuilder createInvestmentScript() {
this.actionName = "CREATE";
this.entityName = "INVESTMENTSCRIPT";
this.entityId = null;
this.href = "/investment/scripts";
return this;
}
public CommandWrapperBuilder updateInvestmentScript(final Long scriptId) {
this.actionName = "UPDATE";
this.entityName = "INVESTMENTSCRIPT";
this.entityId = scriptId;
this.href = "/investment/scripts/" + scriptId;
return this;
}
public CommandWrapperBuilder createInvestmentPurchase() {
this.actionName = "CREATE";
this.entityName = "INVESTMENTPURCHASE";
this.entityId = null;
this.href = "/investment/purchases";
return this;
}
public CommandWrapperBuilder deleteInvestmentPurchase(final Long purchaseId) {
this.actionName = "DELETE";
this.entityName = "INVESTMENTPURCHASE";
this.entityId = purchaseId;
this.href = "/investment/purchases/" + purchaseId;
return this;
}
public CommandWrapperBuilder createChequeStockLot() {
this.actionName = "CREATE";
this.entityName = "CHEQUESTOCKLOT";
@@ -0,0 +1,95 @@
/**
* 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.util.Map;
import java.util.Set;
/**
* Constants for the Treasury Investment masters (Bank / Party Master and Script Master), modelled on the bank's existing
* CBS Investment screens.
*/
public final class InvestmentApiConstants {
private InvestmentApiConstants() {}
public static final String ENTITY_PARTY = "INVESTMENTPARTY";
public static final String ENTITY_SCRIPT = "INVESTMENTSCRIPT";
public static final String RESOURCE_PARTY = "investmentparty";
public static final String RESOURCE_SCRIPT = "investmentscript";
/** Instrument ("Bond Type") an investment party / script belongs to. */
public static final Set<String> BOND_TYPES = Set.of("GOVT_SECURITY", "NON_SLR", "BOND", "MUTUAL_FUND", "FD", "SHARE");
/** Bond types that carry coupon terms (ISIN, interest rate, issue date are mandatory for these). */
public static final Set<String> DEBT_BOND_TYPES = Set.of("GOVT_SECURITY", "NON_SLR", "BOND");
/** SLR classification on the Script Master ("GOI" = Central Government). */
public static final Set<String> SLR_TYPES = Set.of("GOI", "STATE_GOVT", "TREASURY", "NON_SLR");
/** Script Code prefix per bond type, e.g. G00001 for the first Govt Security. */
public static final Map<String, String> SCRIPT_CODE_PREFIX = Map.of("GOVT_SECURITY", "G", "NON_SLR", "N", "BOND", "B", "MUTUAL_FUND",
"M", "FD", "F", "SHARE", "S");
public static final String PARTY_CODE_PREFIX = "PTY-";
// Common parameters
public static final String BOND_TYPE = "bondType";
public static final String GL_ACCOUNT_ID = "glAccountId";
public static final String ADDRESS1 = "address1";
public static final String ADDRESS2 = "address2";
public static final String ADDRESS3 = "address3";
public static final String CITY = "city";
public static final String PINCODE = "pincode";
public static final String PHONE = "phone";
public static final String FAX = "fax";
public static final String ACTIVE = "active";
public static final String LOCALE = "locale";
public static final String DATE_FORMAT = "dateFormat";
// Party parameters
public static final String PARTY_NAME = "partyName";
public static final String BANK_TYPE = "bankType";
public static final String RBI_CODE = "rbiCode";
public static final String EXPOSURE_LIMIT = "limit";
public static final String LEI_NO = "leiNo";
public static final String CP_BANK_NAME = "cpBankName";
public static final String CP_BRANCH_NAME = "cpBranchName";
public static final String CP_ACCOUNT_NO = "cpAccountNo";
public static final String CP_IFSC = "cpIfsc";
// Script parameters
public static final String SLR_TYPE = "slrType";
public static final String SCRIPT_DESC = "scriptDesc";
public static final String ISIN = "isin";
public static final String FACE_VALUE_PER_UNIT = "faceValuePerUnit";
public static final String INT_RATE = "intRate";
public static final String ISSUE_DATE = "issueDate";
public static final String MATURITY_DATE = "maturityDate";
public static final String COMPANY_NAME = "companyName";
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,
LOCALE);
public static final Set<String> SCRIPT_PARAMS = Set.of(BOND_TYPE, SLR_TYPE, SCRIPT_DESC, GL_ACCOUNT_ID, ISIN, FACE_VALUE_PER_UNIT,
INT_RATE, ISSUE_DATE, MATURITY_DATE, COMPANY_NAME, ADDRESS1, ADDRESS2, ADDRESS3, CITY, PINCODE, PHONE, FAX, CORP_CODE,
ACTIVE, LOCALE, DATE_FORMAT);
}
@@ -0,0 +1,99 @@
/**
* 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.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import lombok.RequiredArgsConstructor;
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.InvestmentPartyData;
import org.apache.fineract.portfolio.investment.service.InvestmentMasterReadPlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/investment/parties")
@Component
@Tag(name = "Investment Bank / Party Master", description = "Counterparties an investment is bought from or placed with.")
@RequiredArgsConstructor
public class InvestmentPartyApiResource {
private final PlatformSecurityContext context;
private final InvestmentMasterReadPlatformService readPlatformService;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
@GET
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List investment parties")
public List<InvestmentPartyData> retrieveAll() {
this.context.authenticatedUser().validateHasReadPermission(InvestmentApiConstants.ENTITY_PARTY);
return this.readPlatformService.retrieveAllParties();
}
@GET
@Path("{partyId}")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve an investment party")
public InvestmentPartyData retrieveOne(@PathParam("partyId") @Parameter(description = "partyId") final Long partyId) {
this.context.authenticatedUser().validateHasReadPermission(InvestmentApiConstants.ENTITY_PARTY);
return this.readPlatformService.retrieveParty(partyId);
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Create an investment party")
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper request = new CommandWrapperBuilder().createInvestmentParty().withJson(apiRequestBodyAsJson).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
@PUT
@Path("{partyId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Update an investment party")
public CommandProcessingResult update(@PathParam("partyId") @Parameter(description = "partyId") final Long partyId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper request = new CommandWrapperBuilder().updateInvestmentParty(partyId).withJson(apiRequestBodyAsJson).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
@DELETE
@Path("{partyId}")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Drop an investment party")
public CommandProcessingResult delete(@PathParam("partyId") @Parameter(description = "partyId") final Long partyId) {
final CommandWrapper request = new CommandWrapperBuilder().deleteInvestmentParty(partyId).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
}
@@ -0,0 +1,60 @@
/**
* 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.util.Set;
/** Constants for the Investment Purchase Entry (a deal that creates a holding). */
public final class InvestmentPurchaseApiConstants {
private InvestmentPurchaseApiConstants() {}
public static final String ENTITY = "INVESTMENTPURCHASE";
public static final String RESOURCE = "investmentpurchase";
public static final Set<String> SETTLEMENT_TYPES = Set.of("T+0", "T+1", "T+2");
public static final Set<String> CATEGORIES = Set.of("HTM (Held To Maturity)", "AFS (Available For Sale)", "HFT (Held For Trading)");
public static final String STATUS_ACTIVE = "ACTIVE";
public static final String BOND_TYPE = "bondType";
public static final String SCRIPT_ID = "scriptId";
public static final String PARTY_ID = "partyId";
public static final String DEAL_DATE = "dealDate";
public static final String DEAL_TIME = "dealTime";
public static final String SETTLEMENT_TYPE = "settlementType";
public static final String FACE_VALUE = "faceValue";
public static final String PURCHASE_RATE = "purchaseRate";
public static final String CONSIDER_AMT = "considerAmt";
public static final String NAV = "nav";
public static final String STAMP_DUTY = "stampDuty";
public static final String BROKERAGE = "brokerage";
public static final String CATEGORY = "category";
public static final String LAST_INT_DATE = "lastIntDate";
public static final String CR_GL_ACCOUNT_ID = "crGlAccountId";
public static final String SUB_TYPE = "subType";
/** Optional legacy fields (trade no, authorities, lien, delivery mode, …) kept as a JSON object. */
public static final String OTHER_DETAILS = "otherDetails";
public static final String LOCALE = "locale";
public static final String DATE_FORMAT = "dateFormat";
public static final Set<String> PARAMS = Set.of(BOND_TYPE, SCRIPT_ID, PARTY_ID, DEAL_DATE, DEAL_TIME, SETTLEMENT_TYPE, FACE_VALUE,
PURCHASE_RATE, CONSIDER_AMT, NAV, STAMP_DUTY, BROKERAGE, CATEGORY, LAST_INT_DATE, CR_GL_ACCOUNT_ID, SUB_TYPE, OTHER_DETAILS,
LOCALE, DATE_FORMAT);
}
@@ -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.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.DELETE;
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.core.MediaType;
import java.util.List;
import lombok.RequiredArgsConstructor;
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.InvestmentPurchaseData;
import org.apache.fineract.portfolio.investment.service.InvestmentPurchaseReadPlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/investment/purchases")
@Component
@Tag(name = "Investment Purchase Entry", description = "Investment deals (Govt Security, Bond, Mutual Fund, …) — each becomes a holding.")
@RequiredArgsConstructor
public class InvestmentPurchaseApiResource {
private final PlatformSecurityContext context;
private final InvestmentPurchaseReadPlatformService readPlatformService;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
@GET
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List investment purchases (holdings)")
public List<InvestmentPurchaseData> retrieveAll() {
this.context.authenticatedUser().validateHasReadPermission(InvestmentPurchaseApiConstants.ENTITY);
return this.readPlatformService.retrieveAll();
}
@GET
@Path("{purchaseId}")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve an investment purchase")
public InvestmentPurchaseData retrieveOne(@PathParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId) {
this.context.authenticatedUser().validateHasReadPermission(InvestmentPurchaseApiConstants.ENTITY);
return this.readPlatformService.retrieveOne(purchaseId);
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Save a purchase entry")
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper request = new CommandWrapperBuilder().createInvestmentPurchase().withJson(apiRequestBodyAsJson).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
@DELETE
@Path("{purchaseId}")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Drop a purchase entry")
public CommandProcessingResult delete(@PathParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId) {
final CommandWrapper request = new CommandWrapperBuilder().deleteInvestmentPurchase(purchaseId).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
}
@@ -0,0 +1,89 @@
/**
* 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.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import lombok.RequiredArgsConstructor;
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.InvestmentScriptData;
import org.apache.fineract.portfolio.investment.service.InvestmentMasterReadPlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/investment/scripts")
@Component
@Tag(name = "Investment Script Master", description = "Securities / instruments the bank invests in (Govt Security, Bond, Mutual Fund, …).")
@RequiredArgsConstructor
public class InvestmentScriptApiResource {
private final PlatformSecurityContext context;
private final InvestmentMasterReadPlatformService readPlatformService;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
@GET
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List investment scripts")
public List<InvestmentScriptData> retrieveAll() {
this.context.authenticatedUser().validateHasReadPermission(InvestmentApiConstants.ENTITY_SCRIPT);
return this.readPlatformService.retrieveAllScripts();
}
@GET
@Path("{scriptId}")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve an investment script")
public InvestmentScriptData retrieveOne(@PathParam("scriptId") @Parameter(description = "scriptId") final Long scriptId) {
this.context.authenticatedUser().validateHasReadPermission(InvestmentApiConstants.ENTITY_SCRIPT);
return this.readPlatformService.retrieveScript(scriptId);
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Create an investment script")
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper request = new CommandWrapperBuilder().createInvestmentScript().withJson(apiRequestBodyAsJson).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
@PUT
@Path("{scriptId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Update an investment script")
public CommandProcessingResult update(@PathParam("scriptId") @Parameter(description = "scriptId") final Long scriptId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper request = new CommandWrapperBuilder().updateInvestmentScript(scriptId).withJson(apiRequestBodyAsJson).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
}
@@ -0,0 +1,62 @@
/**
* 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;
import org.apache.fineract.portfolio.investment.domain.InvestmentParty;
@Getter
@Builder
public class InvestmentPartyData {
private final Long id;
private final String partyCode;
private final String bondType;
private final String partyName;
private final String address1;
private final String address2;
private final String address3;
private final String city;
private final String pincode;
private final String phone;
private final String fax;
private final String bankType;
private final Long glAccountId;
private final String glCode;
private final String glName;
private final String rbiCode;
private final BigDecimal limit;
private final String leiNo;
private final String cpBankName;
private final String cpBranchName;
private final String cpAccountNo;
private final String cpIfsc;
private final boolean active;
public static InvestmentPartyData from(final InvestmentParty p) {
return InvestmentPartyData.builder().id(p.getId()).partyCode(p.getPartyCode()).bondType(p.getBondType())
.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())
.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,96 @@
/**
* 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 lombok.Builder;
import lombok.Getter;
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchase;
@Getter
@Builder
public class InvestmentPurchaseData {
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy");
private final Long id;
private final String dealNo;
private final String certNo;
private final String bondType;
private final Long scriptId;
private final String scriptCode;
private final String scriptDesc;
private final String isin;
private final Long partyId;
private final String partyCode;
private final String partyName;
private final String dealDate;
private final String dealTime;
private final String settlementType;
private final String settlementDate;
private final BigDecimal faceValue;
private final BigDecimal purchaseRate;
private final BigDecimal units;
private final BigDecimal balanceUnits;
private final BigDecimal brokerage;
private final BigDecimal stampDuty;
private final BigDecimal bookValue;
private final BigDecimal premiumDiscount;
private final BigDecimal intRate;
private final String issueDate;
private final String maturityDate;
private final String lastIntDate;
private final Integer accruedDays;
private final BigDecimal accruedInterest;
private final BigDecimal settlementAmount;
private final BigDecimal ytm;
private final String category;
private final String subType;
private final Long investmentGlAccountId;
private final String investmentGlCode;
private final String investmentGlName;
private final Long crGlAccountId;
private final String crGlCode;
private final String crGlName;
private final String otherDetails;
private final String status;
public static InvestmentPurchaseData from(final InvestmentPurchase p) {
return InvestmentPurchaseData.builder().id(p.getId()).dealNo(p.getDealNo()).certNo(p.getCertNo()).bondType(p.getBondType())
.scriptId(p.getScript().getId()).scriptCode(p.getScript().getScriptCode()).scriptDesc(p.getScript().getScriptDesc())
.isin(p.getScript().getIsin()).partyId(p.getParty().getId()).partyCode(p.getParty().getPartyCode())
.partyName(p.getParty().getPartyName()).dealDate(format(p.getDealDate())).dealTime(p.getDealTime())
.settlementType(p.getSettlementType()).settlementDate(format(p.getSettlementDate())).faceValue(p.getFaceValue())
.purchaseRate(p.getPurchaseRate()).units(p.getUnits()).balanceUnits(p.getBalanceUnits()).brokerage(p.getBrokerage())
.stampDuty(p.getStampDuty()).bookValue(p.getBookValue()).premiumDiscount(p.getPremiumDiscount()).intRate(p.getIntRate())
.issueDate(format(p.getIssueDate())).maturityDate(format(p.getMaturityDate())).lastIntDate(format(p.getLastIntDate()))
.accruedDays(p.getAccruedDays()).accruedInterest(p.getAccruedInterest()).settlementAmount(p.getSettlementAmount())
.ytm(p.getYtm()).category(p.getCategory()).subType(p.getSubType())
.investmentGlAccountId(p.getInvestmentGlAccount().getId()).investmentGlCode(p.getInvestmentGlAccount().getGlCode())
.investmentGlName(p.getInvestmentGlAccount().getName()).crGlAccountId(p.getCrGlAccount().getId())
.crGlCode(p.getCrGlAccount().getGlCode()).crGlName(p.getCrGlAccount().getName()).otherDetails(p.getOtherDetails())
.status(p.getStatus()).build();
}
private static String format(final LocalDate date) {
return date == null ? null : date.format(DATE_FORMAT);
}
}
@@ -0,0 +1,72 @@
/**
* 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 lombok.Builder;
import lombok.Getter;
import org.apache.fineract.portfolio.investment.domain.InvestmentScript;
@Getter
@Builder
public class InvestmentScriptData {
/** Dates go out as DD-MM-YYYY, the format the Investment screens use throughout. */
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy");
private final Long id;
private final String scriptCode;
private final String bondType;
private final String slrType;
private final String scriptDesc;
private final Long glAccountId;
private final String glCode;
private final String glName;
private final String isin;
private final BigDecimal faceValuePerUnit;
private final BigDecimal intRate;
private final String issueDate;
private final String maturityDate;
private final String companyName;
private final String address1;
private final String address2;
private final String address3;
private final String city;
private final String pincode;
private final String phone;
private final String fax;
private final String corpCode;
private final boolean active;
public static InvestmentScriptData from(final InvestmentScript s) {
return InvestmentScriptData.builder().id(s.getId()).scriptCode(s.getScriptCode()).bondType(s.getBondType())
.slrType(s.getSlrType()).scriptDesc(s.getScriptDesc()).glAccountId(s.getGlAccount().getId())
.glCode(s.getGlAccount().getGlCode()).glName(s.getGlAccount().getName()).isin(s.getIsin())
.faceValuePerUnit(s.getFaceValuePerUnit()).intRate(s.getIntRate()).issueDate(format(s.getIssueDate()))
.maturityDate(format(s.getMaturityDate())).companyName(s.getCompanyName()).address1(s.getAddress1())
.address2(s.getAddress2()).address3(s.getAddress3()).city(s.getCity()).pincode(s.getPincode()).phone(s.getPhone())
.fax(s.getFax()).corpCode(s.getCorpCode()).active(s.isActive()).build();
}
private static String format(final LocalDate date) {
return date == null ? null : date.format(DATE_FORMAT);
}
}
@@ -0,0 +1,105 @@
/**
* 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 lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
/**
* Bank / Party Master — the counterparty an investment is bought from or placed with (bank, RBI, issuer, AMC, broker),
* following the bank's existing CBS Bank / Party Master.
*/
@Entity
@Table(name = "m_investment_party")
@Getter
@Setter
@NoArgsConstructor
public class InvestmentParty extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@Column(name = "party_code", nullable = false, length = 20, unique = true)
private String partyCode;
@Column(name = "bond_type", nullable = false, length = 20)
private String bondType;
@Column(name = "party_name", nullable = false, length = 150)
private String partyName;
@Column(name = "address1", length = 150)
private String address1;
@Column(name = "address2", length = 150)
private String address2;
@Column(name = "address3", length = 150)
private String address3;
@Column(name = "city", length = 100)
private String city;
@Column(name = "pincode", length = 10)
private String pincode;
@Column(name = "phone", length = 20)
private String phone;
@Column(name = "fax", length = 20)
private String fax;
@Column(name = "bank_type", length = 60)
private String bankType;
@ManyToOne
@JoinColumn(name = "gl_account_id", nullable = false)
private GLAccount glAccount;
@Column(name = "rbi_code", length = 30)
private String rbiCode;
@Column(name = "exposure_limit", scale = 6, precision = 19)
private BigDecimal exposureLimit;
@Column(name = "lei_no", length = 30)
private String leiNo;
@Column(name = "cp_bank_name", length = 150)
private String cpBankName;
@Column(name = "cp_branch_name", length = 150)
private String cpBranchName;
@Column(name = "cp_account_no", length = 40)
private String cpAccountNo;
@Column(name = "cp_ifsc", length = 11)
private String cpIfsc;
/** An inactive party is kept for history but not offered for new deals. */
@Column(name = "is_active", nullable = false)
private boolean active = true;
}
@@ -0,0 +1,23 @@
/**
* 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 org.springframework.data.jpa.repository.JpaRepository;
public interface InvestmentPartyRepository extends JpaRepository<InvestmentParty, Long> {}
@@ -0,0 +1,148 @@
/**
* 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;
/**
* Investment Purchase Entry — one deal (as in the bank's existing CBS "Govt.Security/Bond Entry"). Once saved it is the
* holding that interest, sale and redemption later work against; {@link #balanceUnits} tracks what is still held.
* Every derived figure (units, book value, accrued interest, settlement amount) is computed by the backend.
*/
@Entity
@Table(name = "m_investment_purchase")
@Getter
@Setter
@NoArgsConstructor
public class InvestmentPurchase extends AbstractAuditableWithUTCDateTimeCustom<Long> {
/** Financial-year deal number, e.g. 202627000001 for the first deal of FY 2026-27. */
@Column(name = "deal_no", nullable = false, length = 20, unique = true)
private String dealNo;
/** Certificate number — system-generated running number. */
@Column(name = "cert_no", nullable = false, length = 20, unique = true)
private String certNo;
@Column(name = "bond_type", nullable = false, length = 20)
private String bondType;
@ManyToOne
@JoinColumn(name = "script_id", nullable = false)
private InvestmentScript script;
@ManyToOne
@JoinColumn(name = "party_id", nullable = false)
private InvestmentParty party;
@Column(name = "deal_date", nullable = false)
private LocalDate dealDate;
@Column(name = "deal_time", length = 8)
private String dealTime;
@Column(name = "settlement_type", nullable = false, length = 5)
private String settlementType;
@Column(name = "settlement_date", nullable = false)
private LocalDate settlementDate;
@Column(name = "face_value", scale = 6, precision = 19, nullable = false)
private BigDecimal faceValue;
/** Price per ₹100 face value (Govt Security / Bond); NAV for a mutual fund. */
@Column(name = "purchase_rate", scale = 6, precision = 19, nullable = false)
private BigDecimal purchaseRate;
@Column(name = "units", scale = 6, precision = 19, nullable = false)
private BigDecimal units;
@Column(name = "balance_units", scale = 6, precision = 19, nullable = false)
private BigDecimal balanceUnits;
@Column(name = "brokerage", scale = 6, precision = 19)
private BigDecimal brokerage;
@Column(name = "stamp_duty", scale = 6, precision = 19)
private BigDecimal stampDuty;
@Column(name = "book_value", scale = 6, precision = 19, nullable = false)
private BigDecimal bookValue;
@Column(name = "premium_discount", scale = 6, precision = 19)
private BigDecimal premiumDiscount;
@Column(name = "int_rate", scale = 4, precision = 9)
private BigDecimal intRate;
@Column(name = "issue_date")
private LocalDate issueDate;
@Column(name = "maturity_date")
private LocalDate maturityDate;
@Column(name = "last_int_date")
private LocalDate lastIntDate;
@Column(name = "accrued_days")
private Integer accruedDays;
/** Interest paid to the seller from the last coupon date up to the deal date (30/360). */
@Column(name = "accrued_interest", scale = 6, precision = 19)
private BigDecimal accruedInterest;
@Column(name = "settlement_amount", scale = 6, precision = 19, nullable = false)
private BigDecimal settlementAmount;
@Column(name = "ytm", scale = 4, precision = 9)
private BigDecimal ytm;
@Column(name = "category", nullable = false, length = 40)
private String category;
@Column(name = "sub_type", length = 40)
private String subType;
/** Investment GL the holding is booked to (from the script). */
@ManyToOne
@JoinColumn(name = "investment_gl_account_id", nullable = false)
private GLAccount investmentGlAccount;
/** Bank GL the purchase is paid from ("CR. To A/c"). */
@ManyToOne
@JoinColumn(name = "cr_gl_account_id", nullable = false)
private GLAccount crGlAccount;
@Column(name = "other_details", length = 4000)
private String otherDetails;
@Column(name = "status", nullable = false, length = 20)
private String status;
}
@@ -0,0 +1,23 @@
/**
* 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 org.springframework.data.jpa.repository.JpaRepository;
public interface InvestmentPurchaseRepository extends JpaRepository<InvestmentPurchase, Long> {}
@@ -0,0 +1,107 @@
/**
* 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;
/**
* Script Master — what is being bought (a Govt Security, bond, mutual fund scheme, share, …), following the bank's
* existing CBS Script Master. A purchase picks these terms up instead of the dealer re-typing them.
*/
@Entity
@Table(name = "m_investment_script")
@Getter
@Setter
@NoArgsConstructor
public class InvestmentScript extends AbstractAuditableWithUTCDateTimeCustom<Long> {
/** System-generated: bond-type letter + running number, e.g. G00001. */
@Column(name = "script_code", nullable = false, length = 20, unique = true)
private String scriptCode;
@Column(name = "bond_type", nullable = false, length = 20)
private String bondType;
/** GOI / STATE_GOVT / TREASURY / NON_SLR. */
@Column(name = "slr_type", length = 20)
private String slrType;
@Column(name = "script_desc", nullable = false, length = 150)
private String scriptDesc;
@ManyToOne
@JoinColumn(name = "gl_account_id", nullable = false)
private GLAccount glAccount;
@Column(name = "isin", length = 20)
private String isin;
@Column(name = "face_value_per_unit", scale = 6, precision = 19, nullable = false)
private BigDecimal faceValuePerUnit;
@Column(name = "int_rate", scale = 4, precision = 9)
private BigDecimal intRate;
@Column(name = "issue_date")
private LocalDate issueDate;
@Column(name = "maturity_date")
private LocalDate maturityDate;
@Column(name = "company_name", length = 150)
private String companyName;
@Column(name = "address1", length = 150)
private String address1;
@Column(name = "address2", length = 150)
private String address2;
@Column(name = "address3", length = 150)
private String address3;
@Column(name = "city", length = 100)
private String city;
@Column(name = "pincode", length = 10)
private String pincode;
@Column(name = "phone", length = 20)
private String phone;
@Column(name = "fax", length = 20)
private String fax;
@Column(name = "corp_code", length = 30)
private String corpCode;
@Column(name = "is_active", nullable = false)
private boolean active = true;
}
@@ -0,0 +1,23 @@
/**
* 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 org.springframework.data.jpa.repository.JpaRepository;
public interface InvestmentScriptRepository extends JpaRepository<InvestmentScript, Long> {}
@@ -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.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
public class InvestmentPartyNotFoundException extends AbstractPlatformResourceNotFoundException {
public InvestmentPartyNotFoundException(final Long id) {
super("error.msg.investmentparty.id.invalid", "Investment party with identifier " + id + " does not exist", id);
}
}
@@ -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.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
public class InvestmentPurchaseNotFoundException extends AbstractPlatformResourceNotFoundException {
public InvestmentPurchaseNotFoundException(final Long id) {
super("error.msg.investmentpurchase.id.invalid", "Investment purchase with identifier " + id + " does not exist", id);
}
}
@@ -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.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
public class InvestmentScriptNotFoundException extends AbstractPlatformResourceNotFoundException {
public InvestmentScriptNotFoundException(final Long id) {
super("error.msg.investmentscript.id.invalid", "Investment script with identifier " + id + " does not exist", id);
}
}
@@ -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.InvestmentMasterWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "INVESTMENTPARTY", action = "CREATE")
@RequiredArgsConstructor
public class CreateInvestmentPartyCommandHandler implements NewCommandSourceHandler {
private final InvestmentMasterWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.createParty(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.InvestmentPurchaseWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "INVESTMENTPURCHASE", action = "CREATE")
@RequiredArgsConstructor
public class CreateInvestmentPurchaseCommandHandler implements NewCommandSourceHandler {
private final InvestmentPurchaseWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.createPurchase(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.InvestmentMasterWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "INVESTMENTSCRIPT", action = "CREATE")
@RequiredArgsConstructor
public class CreateInvestmentScriptCommandHandler implements NewCommandSourceHandler {
private final InvestmentMasterWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.createScript(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.InvestmentMasterWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "INVESTMENTPARTY", action = "DELETE")
@RequiredArgsConstructor
public class DeleteInvestmentPartyCommandHandler implements NewCommandSourceHandler {
private final InvestmentMasterWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.deleteParty(command.entityId());
}
}
@@ -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.InvestmentPurchaseWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "INVESTMENTPURCHASE", action = "DELETE")
@RequiredArgsConstructor
public class DeleteInvestmentPurchaseCommandHandler implements NewCommandSourceHandler {
private final InvestmentPurchaseWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.deletePurchase(command.entityId());
}
}
@@ -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.InvestmentMasterWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "INVESTMENTPARTY", action = "UPDATE")
@RequiredArgsConstructor
public class UpdateInvestmentPartyCommandHandler implements NewCommandSourceHandler {
private final InvestmentMasterWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.updateParty(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.InvestmentMasterWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "INVESTMENTSCRIPT", action = "UPDATE")
@RequiredArgsConstructor
public class UpdateInvestmentScriptCommandHandler implements NewCommandSourceHandler {
private final InvestmentMasterWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.updateScript(command.entityId(), command);
}
}
@@ -0,0 +1,254 @@
/**
* 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.serialization;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ADDRESS1;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ADDRESS2;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ADDRESS3;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.BANK_TYPE;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.BOND_TYPE;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.BOND_TYPES;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CITY;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.COMPANY_NAME;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CORP_CODE;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CP_ACCOUNT_NO;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CP_BANK_NAME;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CP_BRANCH_NAME;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CP_IFSC;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.DEBT_BOND_TYPES;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.EXPOSURE_LIMIT;
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.INT_RATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ISIN;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ISSUE_DATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.LEI_NO;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.MATURITY_DATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PARTY_NAME;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PARTY_PARAMS;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PHONE;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PINCODE;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.RBI_CODE;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.RESOURCE_PARTY;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.RESOURCE_SCRIPT;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SCRIPT_DESC;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SCRIPT_PARAMS;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SLR_TYPE;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SLR_TYPES;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.time.LocalDate;
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.infrastructure.core.data.ApiParameterError;
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.springframework.stereotype.Component;
/** Validates Bank / Party Master and Script Master requests before anything is written. */
@Component
@RequiredArgsConstructor
public final class InvestmentMasterCommandFromApiJsonDeserializer {
private static final String IFSC_PATTERN = "^[A-Z]{4}0[A-Z0-9]{6}$";
private static final String PINCODE_PATTERN = "^[0-9]{6}$";
private final FromJsonHelper fromApiJsonHelper;
// -------------------- Bank / Party Master --------------------
public void validateForCreateParty(final String json) {
validateParty(json, true);
}
public void validateForUpdateParty(final String json) {
validateParty(json, false);
}
private void validateParty(final String json, final boolean create) {
final JsonElement element = parse(json, PARTY_PARAMS);
final List<ApiParameterError> errors = new ArrayList<>();
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(RESOURCE_PARTY);
validateBondType(element, v, create);
validateRequiredString(element, v, PARTY_NAME, 150, create);
validateGlAccountId(element, v, create);
validateAddress(element, v);
validateOptionalString(element, v, BANK_TYPE, 60);
validateOptionalString(element, v, RBI_CODE, 30);
validateOptionalString(element, v, LEI_NO, 30);
validateOptionalString(element, v, CP_BANK_NAME, 150);
validateOptionalString(element, v, CP_BRANCH_NAME, 150);
validateOptionalString(element, v, CP_ACCOUNT_NO, 40);
final String ifsc = this.fromApiJsonHelper.extractStringNamed(CP_IFSC, element);
if (StringUtils.isNotBlank(ifsc)) {
v.reset().parameter(CP_IFSC).value(ifsc.trim().toUpperCase()).matchesRegularExpression(IFSC_PATTERN,
"IFSC must be 11 characters: 4 letters, 0, then 6 letters/digits");
}
if (this.fromApiJsonHelper.parameterExists(EXPOSURE_LIMIT, element)) {
final BigDecimal limit = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(EXPOSURE_LIMIT, element);
v.reset().parameter(EXPOSURE_LIMIT).value(limit).ignoreIfNull().zeroOrPositiveAmount();
}
throwIfErrors(errors);
}
// -------------------- Script Master --------------------
public void validateForCreateScript(final String json) {
validateScript(json, true);
}
public void validateForUpdateScript(final String json, final String existingBondType) {
validateScript(json, false, existingBondType);
}
private void validateScript(final String json, final boolean create) {
validateScript(json, create, null);
}
private void validateScript(final String json, final boolean create, final String existingBondType) {
final JsonElement element = parse(json, SCRIPT_PARAMS);
final List<ApiParameterError> errors = new ArrayList<>();
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(RESOURCE_SCRIPT);
validateBondType(element, v, create);
validateRequiredString(element, v, SCRIPT_DESC, 150, create);
validateGlAccountId(element, v, create);
final String slrType = this.fromApiJsonHelper.extractStringNamed(SLR_TYPE, element);
if (StringUtils.isNotBlank(slrType)) {
v.reset().parameter(SLR_TYPE).value(slrType).isOneOfTheseValues(SLR_TYPES.toArray());
}
final String bondType = create ? this.fromApiJsonHelper.extractStringNamed(BOND_TYPE, element) : existingBondType;
final boolean debt = bondType != null && DEBT_BOND_TYPES.contains(bondType);
final String isin = this.fromApiJsonHelper.extractStringNamed(ISIN, element);
if (create && debt) {
v.reset().parameter(ISIN).value(isin).notBlank();
}
v.reset().parameter(ISIN).value(isin).ignoreIfNull().notExceedingLengthOf(20);
if (this.fromApiJsonHelper.parameterExists(INT_RATE, element) || (create && debt)) {
final BigDecimal intRate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(INT_RATE, element);
final DataValidatorBuilder rate = v.reset().parameter(INT_RATE).value(intRate);
if (create && debt) {
rate.notNull();
}
rate.ignoreIfNull().zeroOrPositiveAmount();
}
if (this.fromApiJsonHelper.parameterExists(FACE_VALUE_PER_UNIT, element)) {
final BigDecimal fv = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(FACE_VALUE_PER_UNIT, element);
v.reset().parameter(FACE_VALUE_PER_UNIT).value(fv).ignoreIfNull().positiveAmount();
}
LocalDate issueDate = null;
if (this.fromApiJsonHelper.parameterExists(ISSUE_DATE, element) || (create && debt)) {
issueDate = this.fromApiJsonHelper.extractLocalDateNamed(ISSUE_DATE, element);
if (create && debt) {
v.reset().parameter(ISSUE_DATE).value(issueDate).notNull();
}
}
if (this.fromApiJsonHelper.parameterExists(MATURITY_DATE, element)) {
final LocalDate maturityDate = this.fromApiJsonHelper.extractLocalDateNamed(MATURITY_DATE, element);
v.reset().parameter(MATURITY_DATE).value(maturityDate).ignoreIfNull().validateDateAfter(issueDate);
}
validateOptionalString(element, v, COMPANY_NAME, 150);
validateOptionalString(element, v, CORP_CODE, 30);
validateAddress(element, v);
throwIfErrors(errors);
}
// -------------------- shared --------------------
private void validateBondType(final JsonElement element, final DataValidatorBuilder v, final boolean required) {
final String bondType = this.fromApiJsonHelper.extractStringNamed(BOND_TYPE, element);
if (required) {
v.reset().parameter(BOND_TYPE).value(bondType).notBlank();
}
if (StringUtils.isNotBlank(bondType)) {
v.reset().parameter(BOND_TYPE).value(bondType).isOneOfTheseValues(BOND_TYPES.toArray());
}
}
private void validateGlAccountId(final JsonElement element, final DataValidatorBuilder v, final boolean required) {
if (!required && !this.fromApiJsonHelper.parameterExists(GL_ACCOUNT_ID, element)) {
return;
}
final Long glAccountId = this.fromApiJsonHelper.extractLongNamed(GL_ACCOUNT_ID, element);
v.reset().parameter(GL_ACCOUNT_ID).value(glAccountId).notNull().longGreaterThanZero();
}
private void validateRequiredString(final JsonElement element, final DataValidatorBuilder v, final String param, final int maxLength,
final boolean required) {
if (!required && !this.fromApiJsonHelper.parameterExists(param, element)) {
return;
}
final String value = this.fromApiJsonHelper.extractStringNamed(param, element);
v.reset().parameter(param).value(value).notBlank().notExceedingLengthOf(maxLength);
}
private void validateOptionalString(final JsonElement element, final DataValidatorBuilder v, final String param,
final int maxLength) {
final String value = this.fromApiJsonHelper.extractStringNamed(param, element);
v.reset().parameter(param).value(value).ignoreIfNull().notExceedingLengthOf(maxLength);
}
private void validateAddress(final JsonElement element, final DataValidatorBuilder v) {
validateOptionalString(element, v, ADDRESS1, 150);
validateOptionalString(element, v, ADDRESS2, 150);
validateOptionalString(element, v, ADDRESS3, 150);
validateOptionalString(element, v, CITY, 100);
validateOptionalString(element, v, PHONE, 20);
validateOptionalString(element, v, FAX, 20);
final String pincode = this.fromApiJsonHelper.extractStringNamed(PINCODE, element);
if (StringUtils.isNotBlank(pincode)) {
v.reset().parameter(PINCODE).value(pincode.trim()).matchesRegularExpression(PINCODE_PATTERN, "Pincode must be 6 digits");
}
}
private JsonElement parse(final String json, final Set<String> supportedParams) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, supportedParams);
return this.fromApiJsonHelper.parse(json);
}
private void throwIfErrors(final List<ApiParameterError> errors) {
if (!errors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors);
}
}
}
@@ -0,0 +1,140 @@
/**
* 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.serialization;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.BOND_TYPE;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.BROKERAGE;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.CATEGORIES;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.CATEGORY;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.CONSIDER_AMT;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.CR_GL_ACCOUNT_ID;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.DEAL_DATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.DEAL_TIME;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.FACE_VALUE;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.LAST_INT_DATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.NAV;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.OTHER_DETAILS;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.PARAMS;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.PARTY_ID;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.PURCHASE_RATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.RESOURCE;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SCRIPT_ID;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SETTLEMENT_TYPE;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SETTLEMENT_TYPES;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.STAMP_DUTY;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SUB_TYPE;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
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.portfolio.investment.api.InvestmentApiConstants;
import org.springframework.stereotype.Component;
/** Validates a Purchase Entry before anything is computed or written. */
@Component
@RequiredArgsConstructor
public final class InvestmentPurchaseCommandFromApiJsonDeserializer {
private static final String TIME_PATTERN = "^([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$";
private final FromJsonHelper fromApiJsonHelper;
public void validateForCreate(final String json) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, PARAMS);
final JsonElement element = this.fromApiJsonHelper.parse(json);
final List<ApiParameterError> errors = new ArrayList<>();
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(RESOURCE);
final String bondType = this.fromApiJsonHelper.extractStringNamed(BOND_TYPE, element);
v.reset().parameter(BOND_TYPE).value(bondType).notBlank();
if (StringUtils.isNotBlank(bondType)) {
v.reset().parameter(BOND_TYPE).value(bondType).isOneOfTheseValues(InvestmentApiConstants.BOND_TYPES.toArray());
}
v.reset().parameter(SCRIPT_ID).value(this.fromApiJsonHelper.extractLongNamed(SCRIPT_ID, element)).notNull().longGreaterThanZero();
v.reset().parameter(PARTY_ID).value(this.fromApiJsonHelper.extractLongNamed(PARTY_ID, element)).notNull().longGreaterThanZero();
v.reset().parameter(CR_GL_ACCOUNT_ID).value(this.fromApiJsonHelper.extractLongNamed(CR_GL_ACCOUNT_ID, element)).notNull()
.longGreaterThanZero();
v.reset().parameter(DEAL_DATE).value(this.fromApiJsonHelper.extractLocalDateNamed(DEAL_DATE, element)).notNull();
final String settlementType = this.fromApiJsonHelper.extractStringNamed(SETTLEMENT_TYPE, element);
v.reset().parameter(SETTLEMENT_TYPE).value(settlementType).notBlank();
if (StringUtils.isNotBlank(settlementType)) {
v.reset().parameter(SETTLEMENT_TYPE).value(settlementType).isOneOfTheseValues(SETTLEMENT_TYPES.toArray());
}
final String category = this.fromApiJsonHelper.extractStringNamed(CATEGORY, element);
v.reset().parameter(CATEGORY).value(category).notBlank();
if (StringUtils.isNotBlank(category)) {
v.reset().parameter(CATEGORY).value(category).isOneOfTheseValues(CATEGORIES.toArray());
}
if ("MUTUAL_FUND".equals(bondType)) {
v.reset().parameter(CONSIDER_AMT).value(amount(CONSIDER_AMT, element)).notNull().positiveAmount();
v.reset().parameter(NAV).value(amount(NAV, element)).notNull().positiveAmount();
} else {
v.reset().parameter(FACE_VALUE).value(amount(FACE_VALUE, element)).notNull().positiveAmount();
v.reset().parameter(PURCHASE_RATE).value(amount(PURCHASE_RATE, element)).notNull().positiveAmount();
v.reset().parameter(LAST_INT_DATE).value(this.fromApiJsonHelper.extractLocalDateNamed(LAST_INT_DATE, element)).notNull();
}
v.reset().parameter(BROKERAGE).value(amount(BROKERAGE, element)).ignoreIfNull().zeroOrPositiveAmount();
v.reset().parameter(STAMP_DUTY).value(amount(STAMP_DUTY, element)).ignoreIfNull().zeroOrPositiveAmount();
final String dealTime = this.fromApiJsonHelper.extractStringNamed(DEAL_TIME, element);
if (StringUtils.isNotBlank(dealTime)) {
v.reset().parameter(DEAL_TIME).value(dealTime).matchesRegularExpression(TIME_PATTERN, "Deal Time must be HH:mm:ss (24 hr)");
}
v.reset().parameter(SUB_TYPE).value(this.fromApiJsonHelper.extractStringNamed(SUB_TYPE, element)).ignoreIfNull()
.notExceedingLengthOf(40);
if (this.fromApiJsonHelper.parameterExists(OTHER_DETAILS, element)) {
final JsonElement other = element.getAsJsonObject().get(OTHER_DETAILS);
if (!other.isJsonNull() && !other.isJsonObject()) {
v.reset().parameter(OTHER_DETAILS).failWithCode("must.be.an.object");
} else if (other.isJsonObject() && other.toString().length() > 4000) {
v.reset().parameter(OTHER_DETAILS).failWithCode("too.long");
}
}
if (!errors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors);
}
}
private BigDecimal amount(final String param, final JsonElement element) {
return this.fromApiJsonHelper.parameterExists(param, element)
? this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(param, element)
: null;
}
}
@@ -0,0 +1,34 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.investment.service;
import java.util.List;
import org.apache.fineract.portfolio.investment.data.InvestmentPartyData;
import org.apache.fineract.portfolio.investment.data.InvestmentScriptData;
public interface InvestmentMasterReadPlatformService {
List<InvestmentPartyData> retrieveAllParties();
InvestmentPartyData retrieveParty(Long partyId);
List<InvestmentScriptData> retrieveAllScripts();
InvestmentScriptData retrieveScript(Long scriptId);
}
@@ -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.service;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.investment.data.InvestmentPartyData;
import org.apache.fineract.portfolio.investment.data.InvestmentScriptData;
import org.apache.fineract.portfolio.investment.domain.InvestmentPartyRepository;
import org.apache.fineract.portfolio.investment.domain.InvestmentScriptRepository;
import org.apache.fineract.portfolio.investment.exception.InvestmentPartyNotFoundException;
import org.apache.fineract.portfolio.investment.exception.InvestmentScriptNotFoundException;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/** Masters are small (tens of rows), so they are listed whole, newest first, the way the screens show them. */
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class InvestmentMasterReadPlatformServiceImpl implements InvestmentMasterReadPlatformService {
private static final Sort NEWEST_FIRST = Sort.by(Sort.Direction.DESC, "id");
private final InvestmentPartyRepository partyRepository;
private final InvestmentScriptRepository scriptRepository;
@Override
public List<InvestmentPartyData> retrieveAllParties() {
return this.partyRepository.findAll(NEWEST_FIRST).stream().map(InvestmentPartyData::from).toList();
}
@Override
public InvestmentPartyData retrieveParty(final Long partyId) {
return this.partyRepository.findById(partyId).map(InvestmentPartyData::from)
.orElseThrow(() -> new InvestmentPartyNotFoundException(partyId));
}
@Override
public List<InvestmentScriptData> retrieveAllScripts() {
return this.scriptRepository.findAll(NEWEST_FIRST).stream().map(InvestmentScriptData::from).toList();
}
@Override
public InvestmentScriptData retrieveScript(final Long scriptId) {
return this.scriptRepository.findById(scriptId).map(InvestmentScriptData::from)
.orElseThrow(() -> new InvestmentScriptNotFoundException(scriptId));
}
}
@@ -0,0 +1,35 @@
/**
* 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 org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
public interface InvestmentMasterWritePlatformService {
CommandProcessingResult createParty(JsonCommand command);
CommandProcessingResult updateParty(Long partyId, JsonCommand command);
CommandProcessingResult deleteParty(Long partyId);
CommandProcessingResult createScript(JsonCommand command);
CommandProcessingResult updateScript(Long scriptId, JsonCommand command);
}
@@ -0,0 +1,298 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.investment.service;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ACTIVE;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ADDRESS1;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ADDRESS2;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ADDRESS3;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.BANK_TYPE;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.BOND_TYPE;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CITY;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.COMPANY_NAME;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CORP_CODE;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CP_ACCOUNT_NO;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CP_BANK_NAME;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CP_BRANCH_NAME;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CP_IFSC;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.EXPOSURE_LIMIT;
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.INT_RATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ISIN;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ISSUE_DATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.LEI_NO;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.MATURITY_DATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PARTY_CODE_PREFIX;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PARTY_NAME;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PHONE;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PINCODE;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.RBI_CODE;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SCRIPT_CODE_PREFIX;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SCRIPT_DESC;
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SLR_TYPE;
import java.math.BigDecimal;
import java.util.function.Consumer;
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.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.portfolio.investment.domain.InvestmentParty;
import org.apache.fineract.portfolio.investment.domain.InvestmentPartyRepository;
import org.apache.fineract.portfolio.investment.domain.InvestmentScript;
import org.apache.fineract.portfolio.investment.domain.InvestmentScriptRepository;
import org.apache.fineract.portfolio.investment.exception.InvestmentPartyNotFoundException;
import org.apache.fineract.portfolio.investment.exception.InvestmentScriptNotFoundException;
import org.apache.fineract.portfolio.investment.serialization.InvestmentMasterCommandFromApiJsonDeserializer;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class InvestmentMasterWritePlatformServiceImpl implements InvestmentMasterWritePlatformService {
private final InvestmentMasterCommandFromApiJsonDeserializer validator;
private final InvestmentPartyRepository partyRepository;
private final InvestmentScriptRepository scriptRepository;
private final GLAccountRepositoryWrapper glAccountRepository;
private final JdbcTemplate jdbcTemplate;
// -------------------- Bank / Party Master --------------------
@Override
@Transactional
public CommandProcessingResult createParty(final JsonCommand command) {
this.validator.validateForCreateParty(command.json());
final InvestmentParty party = new InvestmentParty();
party.setPartyCode(nextPartyCode());
applyParty(party, command);
if (command.parameterExists(ACTIVE)) {
party.setActive(command.booleanPrimitiveValueOfParameterNamed(ACTIVE));
}
final InvestmentParty saved = saveParty(party);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(saved.getId())
.withResourceIdAsString(saved.getPartyCode()).build();
}
@Override
@Transactional
public CommandProcessingResult updateParty(final Long partyId, final JsonCommand command) {
this.validator.validateForUpdateParty(command.json());
final InvestmentParty party = this.partyRepository.findById(partyId).orElseThrow(() -> new InvestmentPartyNotFoundException(partyId));
applyParty(party, command);
if (command.parameterExists(ACTIVE)) {
party.setActive(command.booleanPrimitiveValueOfParameterNamed(ACTIVE));
}
saveParty(party);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(partyId)
.withResourceIdAsString(party.getPartyCode()).build();
}
@Override
@Transactional
public CommandProcessingResult deleteParty(final Long partyId) {
final InvestmentParty party = this.partyRepository.findById(partyId).orElseThrow(() -> new InvestmentPartyNotFoundException(partyId));
this.partyRepository.delete(party);
this.partyRepository.flush();
return new CommandProcessingResultBuilder().withEntityId(partyId).withResourceIdAsString(party.getPartyCode()).build();
}
/** Copies every party field present in the request onto the entity (absent fields are left unchanged). */
private void applyParty(final InvestmentParty p, final JsonCommand command) {
setIfPresent(command, BOND_TYPE, p::setBondType);
setIfPresent(command, PARTY_NAME, p::setPartyName);
setIfPresent(command, ADDRESS1, p::setAddress1);
setIfPresent(command, ADDRESS2, p::setAddress2);
setIfPresent(command, ADDRESS3, p::setAddress3);
setIfPresent(command, CITY, p::setCity);
setIfPresent(command, PINCODE, p::setPincode);
setIfPresent(command, PHONE, p::setPhone);
setIfPresent(command, FAX, p::setFax);
setIfPresent(command, BANK_TYPE, p::setBankType);
setIfPresent(command, RBI_CODE, p::setRbiCode);
setIfPresent(command, LEI_NO, p::setLeiNo);
setIfPresent(command, CP_BANK_NAME, p::setCpBankName);
setIfPresent(command, CP_BRANCH_NAME, p::setCpBranchName);
setIfPresent(command, CP_ACCOUNT_NO, p::setCpAccountNo);
if (command.parameterExists(CP_IFSC)) {
final String ifsc = command.stringValueOfParameterNamedAllowingNull(CP_IFSC);
p.setCpIfsc(StringUtils.isBlank(ifsc) ? null : ifsc.trim().toUpperCase());
}
if (command.parameterExists(EXPOSURE_LIMIT)) {
p.setExposureLimit(command.bigDecimalValueOfParameterNamed(EXPOSURE_LIMIT));
}
if (command.parameterExists(GL_ACCOUNT_ID)) {
p.setGlAccount(postableAssetGl(command.longValueOfParameterNamed(GL_ACCOUNT_ID)));
}
}
private InvestmentParty saveParty(final InvestmentParty party) {
try {
return this.partyRepository.saveAndFlush(party);
} catch (final DataIntegrityViolationException e) {
throw new PlatformDataIntegrityException("error.msg.investmentparty.duplicate", "A party with this code already exists",
"partyCode", party.getPartyCode());
}
}
/** Next code in the PTY-001, PTY-002 … series. */
private String nextPartyCode() {
final Integer max = this.jdbcTemplate.queryForObject(
"SELECT MAX(CAST(SUBSTRING(party_code FROM 5) AS INTEGER)) FROM m_investment_party WHERE party_code ~ '^PTY-[0-9]+$'",
Integer.class);
return PARTY_CODE_PREFIX + String.format("%03d", (max == null ? 0 : max) + 1);
}
// -------------------- Script Master --------------------
@Override
@Transactional
public CommandProcessingResult createScript(final JsonCommand command) {
this.validator.validateForCreateScript(command.json());
final InvestmentScript script = new InvestmentScript();
script.setBondType(command.stringValueOfParameterNamed(BOND_TYPE));
script.setScriptCode(nextScriptCode(script.getBondType()));
script.setFaceValuePerUnit(defaultFaceValue(script.getBondType()));
applyScript(script, command);
if (command.parameterExists(ACTIVE)) {
script.setActive(command.booleanPrimitiveValueOfParameterNamed(ACTIVE));
}
final InvestmentScript saved = saveScript(script);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(saved.getId())
.withResourceIdAsString(saved.getScriptCode()).build();
}
@Override
@Transactional
public CommandProcessingResult updateScript(final Long scriptId, final JsonCommand command) {
final InvestmentScript script = this.scriptRepository.findById(scriptId)
.orElseThrow(() -> new InvestmentScriptNotFoundException(scriptId));
this.validator.validateForUpdateScript(command.json(), script.getBondType());
if (command.parameterExists(BOND_TYPE) && !script.getBondType().equals(command.stringValueOfParameterNamed(BOND_TYPE))) {
// The script code carries the bond-type letter (G00001), so the type cannot change once created.
throw new GeneralPlatformDomainRuleException("error.msg.investmentscript.bondtype.immutable",
"Bond Type of an existing script cannot be changed", script.getScriptCode());
}
applyScript(script, command);
if (command.parameterExists(ACTIVE)) {
script.setActive(command.booleanPrimitiveValueOfParameterNamed(ACTIVE));
}
saveScript(script);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(scriptId)
.withResourceIdAsString(script.getScriptCode()).build();
}
private void applyScript(final InvestmentScript s, final JsonCommand command) {
setIfPresent(command, SLR_TYPE, s::setSlrType);
setIfPresent(command, SCRIPT_DESC, s::setScriptDesc);
setIfPresent(command, ISIN, s::setIsin);
setIfPresent(command, COMPANY_NAME, s::setCompanyName);
setIfPresent(command, ADDRESS1, s::setAddress1);
setIfPresent(command, ADDRESS2, s::setAddress2);
setIfPresent(command, ADDRESS3, s::setAddress3);
setIfPresent(command, CITY, s::setCity);
setIfPresent(command, PINCODE, s::setPincode);
setIfPresent(command, PHONE, s::setPhone);
setIfPresent(command, FAX, s::setFax);
setIfPresent(command, CORP_CODE, s::setCorpCode);
if (command.parameterExists(FACE_VALUE_PER_UNIT) && command.bigDecimalValueOfParameterNamed(FACE_VALUE_PER_UNIT) != null) {
s.setFaceValuePerUnit(command.bigDecimalValueOfParameterNamed(FACE_VALUE_PER_UNIT));
}
if (command.parameterExists(INT_RATE)) {
s.setIntRate(command.bigDecimalValueOfParameterNamed(INT_RATE));
}
if (command.parameterExists(ISSUE_DATE)) {
s.setIssueDate(command.localDateValueOfParameterNamed(ISSUE_DATE));
}
if (command.parameterExists(MATURITY_DATE)) {
s.setMaturityDate(command.localDateValueOfParameterNamed(MATURITY_DATE));
}
if (s.getIssueDate() != null && s.getMaturityDate() != null && s.getMaturityDate().isBefore(s.getIssueDate())) {
throw new GeneralPlatformDomainRuleException("error.msg.investmentscript.maturity.before.issue",
"Maturity date cannot be before the issue date");
}
if (command.parameterExists(GL_ACCOUNT_ID)) {
s.setGlAccount(postableAssetGl(command.longValueOfParameterNamed(GL_ACCOUNT_ID)));
}
}
private InvestmentScript saveScript(final InvestmentScript script) {
try {
return this.scriptRepository.saveAndFlush(script);
} catch (final DataIntegrityViolationException e) {
throw new PlatformDataIntegrityException("error.msg.investmentscript.duplicate", "A script with this code already exists",
"scriptCode", script.getScriptCode());
}
}
/** Next code for the bond type — its letter plus a five-digit running number, e.g. G00001. */
private String nextScriptCode(final String bondType) {
final String prefix = SCRIPT_CODE_PREFIX.get(bondType);
final Integer max = this.jdbcTemplate.queryForObject(
"SELECT MAX(CAST(SUBSTRING(script_code FROM 2) AS INTEGER)) FROM m_investment_script WHERE script_code ~ ?",
Integer.class, "^" + prefix + "[0-9]{5}$");
return prefix + String.format("%05d", (max == null ? 0 : max) + 1);
}
/** Face value of one unit: ₹100 for securities and bonds, as the existing CBS defaults it. */
private BigDecimal defaultFaceValue(final String bondType) {
return switch (bondType) {
case "MUTUAL_FUND" -> BigDecimal.valueOf(1000);
case "SHARE", "FD" -> BigDecimal.ONE;
default -> BigDecimal.valueOf(100);
};
}
// -------------------- shared --------------------
/** An investment is an asset, so the mapped GL must be an enabled ASSET detail account. */
private GLAccount postableAssetGl(final Long glAccountId) {
final GLAccount gl = this.glAccountRepository.findOneWithNotFoundDetection(glAccountId);
if (gl.isHeaderAccount()) {
throw new GeneralPlatformDomainRuleException("error.msg.investment.glaccount.header",
"GL " + gl.getGlCode() + " is a header account; choose a detail account", glAccountId);
}
if (gl.isDisabled()) {
throw new GeneralPlatformDomainRuleException("error.msg.investment.glaccount.disabled", "GL " + gl.getGlCode() + " is disabled",
glAccountId);
}
if (!GLAccountType.ASSET.getValue().equals(gl.getType())) {
throw new GeneralPlatformDomainRuleException("error.msg.investment.glaccount.not.asset",
"GL " + gl.getGlCode() + " is not an Asset account", glAccountId);
}
return gl;
}
private static void setIfPresent(final JsonCommand command, final String param, final Consumer<String> setter) {
if (command.parameterExists(param)) {
final String value = command.stringValueOfParameterNamedAllowingNull(param);
setter.accept(StringUtils.isBlank(value) ? null : value.trim());
}
}
}
@@ -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.service;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.investment.data.InvestmentPurchaseData;
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchaseRepository;
import org.apache.fineract.portfolio.investment.exception.InvestmentPurchaseNotFoundException;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class InvestmentPurchaseReadPlatformService {
private final InvestmentPurchaseRepository purchaseRepository;
/** All deals, newest first — the Holdings register. */
public List<InvestmentPurchaseData> retrieveAll() {
return this.purchaseRepository.findAll(Sort.by(Sort.Direction.DESC, "id")).stream().map(InvestmentPurchaseData::from).toList();
}
public InvestmentPurchaseData retrieveOne(final Long purchaseId) {
return this.purchaseRepository.findById(purchaseId).map(InvestmentPurchaseData::from)
.orElseThrow(() -> new InvestmentPurchaseNotFoundException(purchaseId));
}
}
@@ -0,0 +1,29 @@
/**
* 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 org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
public interface InvestmentPurchaseWritePlatformService {
CommandProcessingResult createPurchase(JsonCommand command);
CommandProcessingResult deletePurchase(Long purchaseId);
}
@@ -0,0 +1,267 @@
/**
* 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.InvestmentPurchaseApiConstants.BOND_TYPE;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.BROKERAGE;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.CATEGORY;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.CONSIDER_AMT;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.CR_GL_ACCOUNT_ID;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.DEAL_DATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.DEAL_TIME;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.FACE_VALUE;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.LAST_INT_DATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.NAV;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.OTHER_DETAILS;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.PARTY_ID;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.PURCHASE_RATE;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SCRIPT_ID;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SETTLEMENT_TYPE;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.STAMP_DUTY;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.STATUS_ACTIVE;
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SUB_TYPE;
import com.google.gson.JsonElement;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
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.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.portfolio.investment.domain.InvestmentParty;
import org.apache.fineract.portfolio.investment.domain.InvestmentPartyRepository;
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchase;
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchaseRepository;
import org.apache.fineract.portfolio.investment.domain.InvestmentScript;
import org.apache.fineract.portfolio.investment.domain.InvestmentScriptRepository;
import org.apache.fineract.portfolio.investment.exception.InvestmentPartyNotFoundException;
import org.apache.fineract.portfolio.investment.exception.InvestmentPurchaseNotFoundException;
import org.apache.fineract.portfolio.investment.exception.InvestmentScriptNotFoundException;
import org.apache.fineract.portfolio.investment.serialization.InvestmentPurchaseCommandFromApiJsonDeserializer;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* Saves a Purchase Entry as a holding. Every derived figure is computed here from the script master and the deal
* inputs, so the stored deal does not depend on what the screen calculated. No GL entry is posted yet.
*/
@Service
@RequiredArgsConstructor
public class InvestmentPurchaseWritePlatformServiceImpl implements InvestmentPurchaseWritePlatformService {
private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);
private final InvestmentPurchaseCommandFromApiJsonDeserializer validator;
private final InvestmentPurchaseRepository purchaseRepository;
private final InvestmentScriptRepository scriptRepository;
private final InvestmentPartyRepository partyRepository;
private final GLAccountRepositoryWrapper glAccountRepository;
private final JdbcTemplate jdbcTemplate;
@Override
@Transactional
public CommandProcessingResult createPurchase(final JsonCommand command) {
this.validator.validateForCreate(command.json());
final String bondType = command.stringValueOfParameterNamed(BOND_TYPE);
final Long scriptId = command.longValueOfParameterNamed(SCRIPT_ID);
final InvestmentScript script = this.scriptRepository.findById(scriptId)
.orElseThrow(() -> new InvestmentScriptNotFoundException(scriptId));
final Long partyId = command.longValueOfParameterNamed(PARTY_ID);
final InvestmentParty party = this.partyRepository.findById(partyId)
.orElseThrow(() -> new InvestmentPartyNotFoundException(partyId));
if (!script.isActive()) {
throw rule("script.inactive", "Script " + script.getScriptCode() + " is inactive");
}
if (!script.getBondType().equals(bondType)) {
throw rule("script.bondtype.mismatch", "Script " + script.getScriptCode() + " is a " + script.getBondType() + ", not " + bondType);
}
if (!party.isActive()) {
throw rule("party.inactive", "Party " + party.getPartyCode() + " is inactive");
}
final LocalDate dealDate = command.localDateValueOfParameterNamed(DEAL_DATE);
if (script.getMaturityDate() != null && dealDate.isAfter(script.getMaturityDate())) {
throw rule("deal.after.maturity", "Deal date is after the security's maturity date " + script.getMaturityDate());
}
final String settlementType = command.stringValueOfParameterNamed(SETTLEMENT_TYPE);
final InvestmentPurchase p = new InvestmentPurchase();
p.setDealNo(nextDealNo(dealDate));
p.setCertNo(nextCertNo());
p.setBondType(bondType);
p.setScript(script);
p.setParty(party);
p.setDealDate(dealDate);
p.setDealTime(StringUtils.trimToNull(command.stringValueOfParameterNamedAllowingNull(DEAL_TIME)));
p.setSettlementType(settlementType);
p.setSettlementDate(dealDate.plusDays(Long.parseLong(settlementType.substring(2))));
p.setCategory(command.stringValueOfParameterNamed(CATEGORY));
p.setSubType(StringUtils.trimToNull(command.stringValueOfParameterNamedAllowingNull(SUB_TYPE)));
p.setIntRate(script.getIntRate());
p.setIssueDate(script.getIssueDate());
p.setMaturityDate(script.getMaturityDate());
p.setInvestmentGlAccount(script.getGlAccount());
p.setCrGlAccount(postableAssetGl(command.longValueOfParameterNamed(CR_GL_ACCOUNT_ID)));
p.setOtherDetails(otherDetails(command));
p.setStatus(STATUS_ACTIVE);
final BigDecimal brokerage = zeroIfNull(command.bigDecimalValueOfParameterNamed(BROKERAGE));
p.setBrokerage(brokerage);
if ("MUTUAL_FUND".equals(bondType)) {
computeMutualFund(p, command, brokerage);
} else {
computeSecurity(p, command, script, brokerage);
}
final InvestmentPurchase saved = this.purchaseRepository.saveAndFlush(p);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(saved.getId())
.withResourceIdAsString(saved.getDealNo()).build();
}
@Override
@Transactional
public CommandProcessingResult deletePurchase(final Long purchaseId) {
final InvestmentPurchase p = this.purchaseRepository.findById(purchaseId)
.orElseThrow(() -> new InvestmentPurchaseNotFoundException(purchaseId));
if (p.getBalanceUnits().compareTo(p.getUnits()) != 0) {
throw rule("purchase.partly.sold", "Deal " + p.getDealNo() + " has been partly sold and cannot be dropped");
}
this.purchaseRepository.delete(p);
this.purchaseRepository.flush();
return new CommandProcessingResultBuilder().withEntityId(purchaseId).withResourceIdAsString(p.getDealNo()).build();
}
/** Govt Security / Bond: face value at a price per ₹100, plus interest owed to the seller since the last coupon. */
private void computeSecurity(final InvestmentPurchase p, final JsonCommand command, final InvestmentScript script,
final BigDecimal brokerage) {
final BigDecimal faceValue = command.bigDecimalValueOfParameterNamed(FACE_VALUE);
final BigDecimal rate = command.bigDecimalValueOfParameterNamed(PURCHASE_RATE);
final LocalDate lastIntDate = command.localDateValueOfParameterNamed(LAST_INT_DATE);
if (lastIntDate.isAfter(p.getDealDate())) {
throw rule("lastint.after.deal", "Last interest date cannot be after the deal date");
}
final BigDecimal units = faceValue.divide(script.getFaceValuePerUnit(), 6, RoundingMode.HALF_UP);
final BigDecimal cost = faceValue.multiply(rate).divide(HUNDRED, 2, RoundingMode.HALF_UP);
final int days = days360(lastIntDate, p.getDealDate());
final BigDecimal intRate = zeroIfNull(script.getIntRate());
// Interest is counted up to the deal date (the event), not the settlement date — Jitendra Sir.
final BigDecimal accrued = faceValue.multiply(intRate).multiply(BigDecimal.valueOf(days))
.divide(BigDecimal.valueOf(36000), 0, RoundingMode.HALF_UP);
p.setFaceValue(faceValue);
p.setPurchaseRate(rate);
p.setUnits(units);
p.setBalanceUnits(units);
p.setBookValue(cost.add(brokerage));
p.setPremiumDiscount(faceValue.multiply(rate.subtract(HUNDRED)).divide(HUNDRED, 2, RoundingMode.HALF_UP));
p.setLastIntDate(lastIntDate);
p.setAccruedDays(days);
p.setAccruedInterest(accrued);
p.setSettlementAmount(p.getBookValue().add(accrued));
p.setYtm(approxYtm(intRate, rate, script.getIssueDate(), script.getMaturityDate()));
}
/** Mutual fund: the amount invested buys units at the NAV; stamp duty and brokerage add to the cost. */
private void computeMutualFund(final InvestmentPurchase p, final JsonCommand command, final BigDecimal brokerage) {
final BigDecimal amount = command.bigDecimalValueOfParameterNamed(CONSIDER_AMT);
final BigDecimal nav = command.bigDecimalValueOfParameterNamed(NAV);
final BigDecimal stampDuty = zeroIfNull(command.bigDecimalValueOfParameterNamed(STAMP_DUTY));
final BigDecimal units = amount.divide(nav, 3, RoundingMode.HALF_UP);
p.setFaceValue(amount);
p.setPurchaseRate(nav);
p.setUnits(units);
p.setBalanceUnits(units);
p.setStampDuty(stampDuty);
p.setBookValue(amount.add(stampDuty).add(brokerage));
p.setAccruedInterest(BigDecimal.ZERO);
p.setAccruedDays(0);
p.setSettlementAmount(p.getBookValue());
}
/** Days between two dates on a 30/360 basis (every month 30 days), as the bank's existing CBS counts them. */
static int days360(final LocalDate from, final LocalDate to) {
if (from == null || to == null || !to.isAfter(from)) {
return 0;
}
final int d1 = Math.min(from.getDayOfMonth(), 30);
final int d2 = Math.min(to.getDayOfMonth(), 30);
return (to.getYear() - from.getYear()) * 360 + (to.getMonthValue() - from.getMonthValue()) * 30 + (d2 - d1);
}
/** Approximate yield: coupon adjusted for the discount/premium spread over the security's life, over the price. */
private static BigDecimal approxYtm(final BigDecimal coupon, final BigDecimal price, final LocalDate issue, final LocalDate maturity) {
if (coupon.signum() == 0 || issue == null || maturity == null || !maturity.isAfter(issue)) {
return coupon;
}
final double years = ChronoUnit.DAYS.between(issue, maturity) / 365.25;
final double ytm = (coupon.doubleValue() + (100 - price.doubleValue()) / years) / price.doubleValue() * 100;
return BigDecimal.valueOf(ytm).setScale(2, RoundingMode.HALF_UP);
}
/** Financial-year deal number: FY start year + FY end (2 digits) + 6-digit running number, e.g. 202627000001. */
private String nextDealNo(final LocalDate dealDate) {
final int fyStart = dealDate.getMonthValue() >= 4 ? dealDate.getYear() : dealDate.getYear() - 1;
final String prefix = fyStart + String.format("%02d", (fyStart + 1) % 100);
final Integer max = this.jdbcTemplate.queryForObject(
"SELECT MAX(CAST(SUBSTRING(deal_no FROM 7) AS INTEGER)) FROM m_investment_purchase WHERE deal_no LIKE ?", Integer.class,
prefix + "%");
return prefix + String.format("%06d", (max == null ? 0 : max) + 1);
}
private String nextCertNo() {
final Integer max = this.jdbcTemplate.queryForObject(
"SELECT MAX(CAST(cert_no AS INTEGER)) FROM m_investment_purchase WHERE cert_no ~ '^[0-9]+$'", Integer.class);
return String.valueOf((max == null ? 0 : max) + 1);
}
private static String otherDetails(final JsonCommand command) {
if (!command.parameterExists(OTHER_DETAILS)) {
return null;
}
final JsonElement other = command.parsedJson().getAsJsonObject().get(OTHER_DETAILS);
return other == null || other.isJsonNull() ? null : other.toString();
}
/** The paying bank account must be an enabled ASSET detail GL. */
private GLAccount postableAssetGl(final Long glAccountId) {
final GLAccount gl = this.glAccountRepository.findOneWithNotFoundDetection(glAccountId);
if (gl.isHeaderAccount() || gl.isDisabled() || !GLAccountType.ASSET.getValue().equals(gl.getType())) {
throw rule("cr.glaccount.invalid", "CR. To A/c " + gl.getGlCode() + " must be an enabled Asset detail account");
}
return gl;
}
private static BigDecimal zeroIfNull(final BigDecimal value) {
return value == null ? BigDecimal.ZERO : value;
}
private static GeneralPlatformDomainRuleException rule(final String code, final String message) {
return new GeneralPlatformDomainRuleException("error.msg.investmentpurchase." + code, message);
}
}
@@ -165,4 +165,6 @@
<include file="parts/0590_savings_interest_posting_report_by_branch.xml" relativeToChangelogFile="true"/>
<include file="parts/0591_cashier_allocation_time_and_deposit_counter_office.xml" relativeToChangelogFile="true"/>
<include file="parts/0592_enable_teller_cashier_maker_checker.xml" relativeToChangelogFile="true"/>
<include file="parts/0593_investment_masters.xml" relativeToChangelogFile="true"/>
<include file="parts/0594_investment_purchase.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
@@ -0,0 +1,216 @@
<?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">
<!--
Treasury Investment masters, following the bank's existing CBS screens:
- m_investment_party : Bank / Party Master (whom an investment is bought from / placed with)
- m_investment_script : Script Master (what is bought - Govt Security, Bond, Mutual Fund, ...)
Party and script codes are generated by the application (PTY-001, G00001). Both rows are
tied to the investment (asset) GL they are booked to.
-->
<changeSet author="cbs-anshul" id="0593-1-create-investment-party">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_investment_party"/></not>
</preConditions>
<createTable tableName="m_investment_party">
<column name="id" type="BIGINT" autoIncrement="true"><constraints nullable="false" primaryKey="true"/></column>
<column name="party_code" type="VARCHAR(20)"><constraints nullable="false"/></column>
<column name="bond_type" type="VARCHAR(20)"><constraints nullable="false"/></column>
<column name="party_name" type="VARCHAR(150)"><constraints nullable="false"/></column>
<column name="address1" type="VARCHAR(150)"/>
<column name="address2" type="VARCHAR(150)"/>
<column name="address3" type="VARCHAR(150)"/>
<column name="city" type="VARCHAR(100)"/>
<column name="pincode" type="VARCHAR(10)"/>
<column name="phone" type="VARCHAR(20)"/>
<column name="fax" type="VARCHAR(20)"/>
<column name="bank_type" type="VARCHAR(60)"/>
<column name="gl_account_id" type="BIGINT"><constraints nullable="false"/></column>
<column name="rbi_code" type="VARCHAR(30)"/>
<column name="exposure_limit" type="DECIMAL(19,6)"/>
<column name="lei_no" type="VARCHAR(30)"/>
<column name="cp_bank_name" type="VARCHAR(150)"/>
<column name="cp_branch_name" type="VARCHAR(150)"/>
<column name="cp_account_no" type="VARCHAR(40)"/>
<column name="cp_ifsc" type="VARCHAR(11)"/>
<column name="is_active" type="BOOLEAN" defaultValueBoolean="true"><constraints nullable="false"/></column>
<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>
<addUniqueConstraint tableName="m_investment_party" columnNames="party_code" constraintName="uq_investment_party_code"/>
<addForeignKeyConstraint baseTableName="m_investment_party" baseColumnNames="gl_account_id" constraintName="fk_investment_party_gl_account"
referencedTableName="acc_gl_account" referencedColumnNames="id"/>
<createIndex tableName="m_investment_party" indexName="idx_investment_party_bond_type">
<column name="bond_type"/>
</createIndex>
</changeSet>
<changeSet author="cbs-anshul" id="0593-2-create-investment-script">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_investment_script"/></not>
</preConditions>
<createTable tableName="m_investment_script">
<column name="id" type="BIGINT" autoIncrement="true"><constraints nullable="false" primaryKey="true"/></column>
<column name="script_code" type="VARCHAR(20)"><constraints nullable="false"/></column>
<column name="bond_type" type="VARCHAR(20)"><constraints nullable="false"/></column>
<column name="slr_type" type="VARCHAR(20)"/>
<column name="script_desc" type="VARCHAR(150)"><constraints nullable="false"/></column>
<column name="gl_account_id" type="BIGINT"><constraints nullable="false"/></column>
<column name="isin" type="VARCHAR(20)"/>
<column name="face_value_per_unit" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
<column name="int_rate" type="DECIMAL(9,4)"/>
<column name="issue_date" type="DATE"/>
<column name="maturity_date" type="DATE"/>
<column name="company_name" type="VARCHAR(150)"/>
<column name="address1" type="VARCHAR(150)"/>
<column name="address2" type="VARCHAR(150)"/>
<column name="address3" type="VARCHAR(150)"/>
<column name="city" type="VARCHAR(100)"/>
<column name="pincode" type="VARCHAR(10)"/>
<column name="phone" type="VARCHAR(20)"/>
<column name="fax" type="VARCHAR(20)"/>
<column name="corp_code" type="VARCHAR(30)"/>
<column name="is_active" type="BOOLEAN" defaultValueBoolean="true"><constraints nullable="false"/></column>
<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>
<addUniqueConstraint tableName="m_investment_script" columnNames="script_code" constraintName="uq_investment_script_code"/>
<addForeignKeyConstraint baseTableName="m_investment_script" baseColumnNames="gl_account_id" constraintName="fk_investment_script_gl_account"
referencedTableName="acc_gl_account" referencedColumnNames="id"/>
<createIndex tableName="m_investment_script" indexName="idx_investment_script_bond_type">
<column name="bond_type"/>
</createIndex>
</changeSet>
<changeSet author="cbs-anshul" id="0593-3-investment-master-permissions">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code IN ('READ_INVESTMENTPARTY', 'CREATE_INVESTMENTPARTY', 'UPDATE_INVESTMENTPARTY', 'DELETE_INVESTMENTPARTY', 'READ_INVESTMENTSCRIPT', 'CREATE_INVESTMENTSCRIPT', 'UPDATE_INVESTMENTSCRIPT', 'CREATE_INVESTMENTPARTY_CHECKER', 'UPDATE_INVESTMENTPARTY_CHECKER', 'DELETE_INVESTMENTPARTY_CHECKER', 'CREATE_INVESTMENTSCRIPT_CHECKER', 'UPDATE_INVESTMENTSCRIPT_CHECKER')</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="READ_INVESTMENTPARTY"/>
<column name="entity_name" value="INVESTMENTPARTY"/>
<column name="action_name" value="READ"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="CREATE_INVESTMENTPARTY"/>
<column name="entity_name" value="INVESTMENTPARTY"/>
<column name="action_name" value="CREATE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="UPDATE_INVESTMENTPARTY"/>
<column name="entity_name" value="INVESTMENTPARTY"/>
<column name="action_name" value="UPDATE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="DELETE_INVESTMENTPARTY"/>
<column name="entity_name" value="INVESTMENTPARTY"/>
<column name="action_name" value="DELETE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="READ_INVESTMENTSCRIPT"/>
<column name="entity_name" value="INVESTMENTSCRIPT"/>
<column name="action_name" value="READ"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="CREATE_INVESTMENTSCRIPT"/>
<column name="entity_name" value="INVESTMENTSCRIPT"/>
<column name="action_name" value="CREATE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="UPDATE_INVESTMENTSCRIPT"/>
<column name="entity_name" value="INVESTMENTSCRIPT"/>
<column name="action_name" value="UPDATE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="CREATE_INVESTMENTPARTY_CHECKER"/>
<column name="entity_name" value="INVESTMENTPARTY"/>
<column name="action_name" value="CREATE_CHECKER"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="UPDATE_INVESTMENTPARTY_CHECKER"/>
<column name="entity_name" value="INVESTMENTPARTY"/>
<column name="action_name" value="UPDATE_CHECKER"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="DELETE_INVESTMENTPARTY_CHECKER"/>
<column name="entity_name" value="INVESTMENTPARTY"/>
<column name="action_name" value="DELETE_CHECKER"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="CREATE_INVESTMENTSCRIPT_CHECKER"/>
<column name="entity_name" value="INVESTMENTSCRIPT"/>
<column name="action_name" value="CREATE_CHECKER"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="UPDATE_INVESTMENTSCRIPT_CHECKER"/>
<column name="entity_name" value="INVESTMENTSCRIPT"/>
<column name="action_name" value="UPDATE_CHECKER"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
<!-- Whoever already sees the Investment menu gets to read and maintain its masters. Maker-checker
is left off (can_maker_checker=false); it can be switched on per action from
Configure Maker Checker Tasks, and the _CHECKER rows above let a checker role approve. -->
<changeSet author="cbs-anshul" id="0593-4-grant-investment-master-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_INVESTMENTPARTY', 'CREATE_INVESTMENTPARTY', 'UPDATE_INVESTMENTPARTY', 'DELETE_INVESTMENTPARTY', 'READ_INVESTMENTSCRIPT', 'CREATE_INVESTMENTSCRIPT', 'UPDATE_INVESTMENTSCRIPT')
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>
@@ -0,0 +1,149 @@
<?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">
<!--
Investment Purchase Entry: one row per deal, which is the holding that interest, sale and
redemption later work against (balance_units tracks what is still held). Units, book value,
accrued interest and settlement amount are computed by the application. The optional legacy
deal fields (trade no, authorities, lien, delivery mode, ...) are kept in other_details as JSON.
No GL entry is posted by this table yet.
-->
<changeSet author="cbs-anshul" id="0594-1-create-investment-purchase">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_investment_purchase"/></not>
</preConditions>
<createTable tableName="m_investment_purchase">
<column name="id" type="BIGINT" autoIncrement="true"><constraints nullable="false" primaryKey="true"/></column>
<column name="deal_no" type="VARCHAR(20)"><constraints nullable="false"/></column>
<column name="cert_no" type="VARCHAR(20)"><constraints nullable="false"/></column>
<column name="bond_type" type="VARCHAR(20)"><constraints nullable="false"/></column>
<column name="script_id" type="BIGINT"><constraints nullable="false"/></column>
<column name="party_id" type="BIGINT"><constraints nullable="false"/></column>
<column name="deal_date" type="DATE"><constraints nullable="false"/></column>
<column name="deal_time" type="VARCHAR(8)"/>
<column name="settlement_type" type="VARCHAR(5)"><constraints nullable="false"/></column>
<column name="settlement_date" type="DATE"><constraints nullable="false"/></column>
<column name="face_value" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
<column name="purchase_rate" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
<column name="units" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
<column name="balance_units" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
<column name="brokerage" type="DECIMAL(19,6)"/>
<column name="stamp_duty" type="DECIMAL(19,6)"/>
<column name="book_value" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
<column name="premium_discount" type="DECIMAL(19,6)"/>
<column name="int_rate" type="DECIMAL(9,4)"/>
<column name="issue_date" type="DATE"/>
<column name="maturity_date" type="DATE"/>
<column name="last_int_date" type="DATE"/>
<column name="accrued_days" type="INT"/>
<column name="accrued_interest" type="DECIMAL(19,6)"/>
<column name="settlement_amount" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
<column name="ytm" type="DECIMAL(9,4)"/>
<column name="category" type="VARCHAR(40)"><constraints nullable="false"/></column>
<column name="sub_type" type="VARCHAR(40)"/>
<column name="investment_gl_account_id" type="BIGINT"><constraints nullable="false"/></column>
<column name="cr_gl_account_id" type="BIGINT"><constraints nullable="false"/></column>
<column name="other_details" type="VARCHAR(4000)"/>
<column name="status" type="VARCHAR(20)"><constraints nullable="false"/></column>
<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>
<addUniqueConstraint tableName="m_investment_purchase" columnNames="deal_no" constraintName="uq_investment_purchase_deal_no"/>
<addUniqueConstraint tableName="m_investment_purchase" columnNames="cert_no" constraintName="uq_investment_purchase_cert_no"/>
<addForeignKeyConstraint baseTableName="m_investment_purchase" baseColumnNames="script_id" constraintName="fk_investment_purchase_script"
referencedTableName="m_investment_script" referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="m_investment_purchase" baseColumnNames="party_id" constraintName="fk_investment_purchase_party"
referencedTableName="m_investment_party" referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="m_investment_purchase" baseColumnNames="investment_gl_account_id" constraintName="fk_investment_purchase_inv_gl"
referencedTableName="acc_gl_account" referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="m_investment_purchase" baseColumnNames="cr_gl_account_id" constraintName="fk_investment_purchase_cr_gl"
referencedTableName="acc_gl_account" referencedColumnNames="id"/>
<createIndex tableName="m_investment_purchase" indexName="idx_investment_purchase_script">
<column name="script_id"/>
</createIndex>
<createIndex tableName="m_investment_purchase" indexName="idx_investment_purchase_bond_type_status">
<column name="bond_type"/>
<column name="status"/>
</createIndex>
</changeSet>
<changeSet author="cbs-anshul" id="0594-2-investment-purchase-permissions">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code IN ('READ_INVESTMENTPURCHASE', 'CREATE_INVESTMENTPURCHASE', 'DELETE_INVESTMENTPURCHASE', 'CREATE_INVESTMENTPURCHASE_CHECKER', 'DELETE_INVESTMENTPURCHASE_CHECKER')</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="READ_INVESTMENTPURCHASE"/>
<column name="entity_name" value="INVESTMENTPURCHASE"/>
<column name="action_name" value="READ"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="CREATE_INVESTMENTPURCHASE"/>
<column name="entity_name" value="INVESTMENTPURCHASE"/>
<column name="action_name" value="CREATE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="DELETE_INVESTMENTPURCHASE"/>
<column name="entity_name" value="INVESTMENTPURCHASE"/>
<column name="action_name" value="DELETE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="CREATE_INVESTMENTPURCHASE_CHECKER"/>
<column name="entity_name" value="INVESTMENTPURCHASE"/>
<column name="action_name" value="CREATE_CHECKER"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="DELETE_INVESTMENTPURCHASE_CHECKER"/>
<column name="entity_name" value="INVESTMENTPURCHASE"/>
<column name="action_name" value="DELETE_CHECKER"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
<!-- Same roles as the masters: whoever sees the Investment menu. Maker-checker stays off until
switched on from Configure Maker Checker Tasks. -->
<changeSet author="cbs-anshul" id="0594-3-grant-investment-purchase-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_INVESTMENTPURCHASE', 'CREATE_INVESTMENTPURCHASE', 'DELETE_INVESTMENTPURCHASE')
AND NOT EXISTS (SELECT 1 FROM m_role_permission x WHERE x.role_id = rp.role_id AND x.permission_id = p.id)
</sql>
</changeSet>
</databaseChangeLog>