diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index 0f2140efd..a51f7d09b 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -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"; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentApiConstants.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentApiConstants.java new file mode 100644 index 000000000..51b72428d --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentApiConstants.java @@ -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 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 DEBT_BOND_TYPES = Set.of("GOVT_SECURITY", "NON_SLR", "BOND"); + + /** SLR classification on the Script Master ("GOI" = Central Government). */ + public static final Set 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 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 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 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); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentPartyApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentPartyApiResource.java new file mode 100644 index 000000000..7030c76b0 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentPartyApiResource.java @@ -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 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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentPurchaseApiConstants.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentPurchaseApiConstants.java new file mode 100644 index 000000000..0d707a930 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentPurchaseApiConstants.java @@ -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 SETTLEMENT_TYPES = Set.of("T+0", "T+1", "T+2"); + public static final Set 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 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); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentPurchaseApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentPurchaseApiResource.java new file mode 100644 index 000000000..b1ccfdb17 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentPurchaseApiResource.java @@ -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 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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentScriptApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentScriptApiResource.java new file mode 100644 index 000000000..a689a46e2 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentScriptApiResource.java @@ -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 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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/data/InvestmentPartyData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/data/InvestmentPartyData.java new file mode 100644 index 000000000..d74792478 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/data/InvestmentPartyData.java @@ -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(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/data/InvestmentPurchaseData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/data/InvestmentPurchaseData.java new file mode 100644 index 000000000..87ea0c541 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/data/InvestmentPurchaseData.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/data/InvestmentScriptData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/data/InvestmentScriptData.java new file mode 100644 index 000000000..e280e82bb --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/data/InvestmentScriptData.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/domain/InvestmentParty.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/domain/InvestmentParty.java new file mode 100644 index 000000000..9187bf261 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/domain/InvestmentParty.java @@ -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 { + + @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; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/domain/InvestmentPartyRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/domain/InvestmentPartyRepository.java new file mode 100644 index 000000000..f37b3a915 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/domain/InvestmentPartyRepository.java @@ -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 {} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/domain/InvestmentPurchase.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/domain/InvestmentPurchase.java new file mode 100644 index 000000000..c9aa74ba4 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/domain/InvestmentPurchase.java @@ -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 { + + /** 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; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/domain/InvestmentPurchaseRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/domain/InvestmentPurchaseRepository.java new file mode 100644 index 000000000..ef9a4acf5 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/domain/InvestmentPurchaseRepository.java @@ -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 {} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/domain/InvestmentScript.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/domain/InvestmentScript.java new file mode 100644 index 000000000..75e5f7431 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/domain/InvestmentScript.java @@ -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 { + + /** 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; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/domain/InvestmentScriptRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/domain/InvestmentScriptRepository.java new file mode 100644 index 000000000..274fe4381 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/domain/InvestmentScriptRepository.java @@ -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 {} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/exception/InvestmentPartyNotFoundException.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/exception/InvestmentPartyNotFoundException.java new file mode 100644 index 000000000..1aa9b8fbe --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/exception/InvestmentPartyNotFoundException.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/exception/InvestmentPurchaseNotFoundException.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/exception/InvestmentPurchaseNotFoundException.java new file mode 100644 index 000000000..18bef8068 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/exception/InvestmentPurchaseNotFoundException.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/exception/InvestmentScriptNotFoundException.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/exception/InvestmentScriptNotFoundException.java new file mode 100644 index 000000000..3c49ec720 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/exception/InvestmentScriptNotFoundException.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/handler/CreateInvestmentPartyCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/handler/CreateInvestmentPartyCommandHandler.java new file mode 100644 index 000000000..fe2af37b2 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/handler/CreateInvestmentPartyCommandHandler.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/handler/CreateInvestmentPurchaseCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/handler/CreateInvestmentPurchaseCommandHandler.java new file mode 100644 index 000000000..d90e5b4f5 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/handler/CreateInvestmentPurchaseCommandHandler.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/handler/CreateInvestmentScriptCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/handler/CreateInvestmentScriptCommandHandler.java new file mode 100644 index 000000000..a8f11b7e9 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/handler/CreateInvestmentScriptCommandHandler.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/handler/DeleteInvestmentPartyCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/handler/DeleteInvestmentPartyCommandHandler.java new file mode 100644 index 000000000..272423aee --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/handler/DeleteInvestmentPartyCommandHandler.java @@ -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()); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/handler/DeleteInvestmentPurchaseCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/handler/DeleteInvestmentPurchaseCommandHandler.java new file mode 100644 index 000000000..6947df6b9 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/handler/DeleteInvestmentPurchaseCommandHandler.java @@ -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()); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/handler/UpdateInvestmentPartyCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/handler/UpdateInvestmentPartyCommandHandler.java new file mode 100644 index 000000000..aeb6486cb --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/handler/UpdateInvestmentPartyCommandHandler.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/handler/UpdateInvestmentScriptCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/handler/UpdateInvestmentScriptCommandHandler.java new file mode 100644 index 000000000..f358c6418 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/handler/UpdateInvestmentScriptCommandHandler.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/serialization/InvestmentMasterCommandFromApiJsonDeserializer.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/serialization/InvestmentMasterCommandFromApiJsonDeserializer.java new file mode 100644 index 000000000..f33827742 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/serialization/InvestmentMasterCommandFromApiJsonDeserializer.java @@ -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 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 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 supportedParams) { + if (StringUtils.isBlank(json)) { + throw new InvalidJsonException(); + } + final Type typeOfMap = new TypeToken>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, supportedParams); + return this.fromApiJsonHelper.parse(json); + } + + private void throwIfErrors(final List errors) { + if (!errors.isEmpty()) { + throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/serialization/InvestmentPurchaseCommandFromApiJsonDeserializer.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/serialization/InvestmentPurchaseCommandFromApiJsonDeserializer.java new file mode 100644 index 000000000..d30ba0eec --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/serialization/InvestmentPurchaseCommandFromApiJsonDeserializer.java @@ -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>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, PARAMS); + final JsonElement element = this.fromApiJsonHelper.parse(json); + + final List 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; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentMasterReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentMasterReadPlatformService.java new file mode 100644 index 000000000..148913036 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentMasterReadPlatformService.java @@ -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 retrieveAllParties(); + + InvestmentPartyData retrieveParty(Long partyId); + + List retrieveAllScripts(); + + InvestmentScriptData retrieveScript(Long scriptId); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentMasterReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentMasterReadPlatformServiceImpl.java new file mode 100644 index 000000000..84ae8c778 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentMasterReadPlatformServiceImpl.java @@ -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 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 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)); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentMasterWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentMasterWritePlatformService.java new file mode 100644 index 000000000..7e31d6f67 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentMasterWritePlatformService.java @@ -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); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentMasterWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentMasterWritePlatformServiceImpl.java new file mode 100644 index 000000000..a8cb4d024 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentMasterWritePlatformServiceImpl.java @@ -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 setter) { + if (command.parameterExists(param)) { + final String value = command.stringValueOfParameterNamedAllowingNull(param); + setter.accept(StringUtils.isBlank(value) ? null : value.trim()); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentPurchaseReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentPurchaseReadPlatformService.java new file mode 100644 index 000000000..33c38e2cf --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentPurchaseReadPlatformService.java @@ -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 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)); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentPurchaseWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentPurchaseWritePlatformService.java new file mode 100644 index 000000000..61d495d21 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentPurchaseWritePlatformService.java @@ -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); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentPurchaseWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentPurchaseWritePlatformServiceImpl.java new file mode 100644 index 000000000..973db29dc --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentPurchaseWritePlatformServiceImpl.java @@ -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); + } +} diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml index 0f961c93b..30d56205f 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml @@ -165,4 +165,6 @@ + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0593_investment_masters.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0593_investment_masters.xml new file mode 100644 index 000000000..bbe223c24 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0593_investment_masters.xml @@ -0,0 +1,216 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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') + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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) + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0594_investment_purchase.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0594_investment_purchase.xml new file mode 100644 index 000000000..a7fa0c225 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0594_investment_purchase.xml @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SELECT COUNT(*) FROM m_permission WHERE code IN ('READ_INVESTMENTPURCHASE', 'CREATE_INVESTMENTPURCHASE', 'DELETE_INVESTMENTPURCHASE', 'CREATE_INVESTMENTPURCHASE_CHECKER', 'DELETE_INVESTMENTPURCHASE_CHECKER') + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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) + + +