feat(investment): sell, redemption, write-off, GOI transfer and premium amortisation
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Waiting to run
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Waiting to run
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Waiting to run
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Waiting to run
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Waiting to run
Fineract Documentation build / build (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Waiting to run
Fineract Liquibase Only mode - PostgreSQL / test (push) Waiting to run
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Waiting to run
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Waiting to run
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Waiting to run
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Waiting to run
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Waiting to run
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Waiting to run
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Waiting to run
Fineract Documentation build / build (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Waiting to run
Fineract Liquibase Only mode - PostgreSQL / test (push) Waiting to run
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Waiting to run
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Waiting to run
This commit is contained in:
+56
@@ -159,6 +159,62 @@ public class CommandWrapperBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder createInvestmentSale() {
|
||||||
|
this.actionName = "CREATE";
|
||||||
|
this.entityName = "INVESTMENTSALE";
|
||||||
|
this.entityId = null;
|
||||||
|
this.href = "/investment/sales";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder settleInvestmentSale(final Long saleId) {
|
||||||
|
this.actionName = "SETTLE";
|
||||||
|
this.entityName = "INVESTMENTSALE";
|
||||||
|
this.entityId = saleId;
|
||||||
|
this.href = "/investment/sales/" + saleId + "/settle";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder deleteInvestmentSale(final Long saleId) {
|
||||||
|
this.actionName = "DELETE";
|
||||||
|
this.entityName = "INVESTMENTSALE";
|
||||||
|
this.entityId = saleId;
|
||||||
|
this.href = "/investment/sales/" + saleId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder amortiseInvestmentPremium() {
|
||||||
|
this.actionName = "AMORTISE";
|
||||||
|
this.entityName = "INVESTMENTSALE";
|
||||||
|
this.entityId = null;
|
||||||
|
this.href = "/investment/amortisation";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder writeOffInvestmentPurchase(final Long purchaseId) {
|
||||||
|
this.actionName = "WRITEOFF";
|
||||||
|
this.entityName = "INVESTMENTSALE";
|
||||||
|
this.entityId = purchaseId;
|
||||||
|
this.href = "/investment/purchases/" + purchaseId + "/write-off";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder redeemInvestmentPurchase(final Long purchaseId) {
|
||||||
|
this.actionName = "REDEEM";
|
||||||
|
this.entityName = "INVESTMENTSALE";
|
||||||
|
this.entityId = purchaseId;
|
||||||
|
this.href = "/investment/purchases/" + purchaseId + "/redeem";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder transferInvestmentSecurity(final Long purchaseId) {
|
||||||
|
this.actionName = "CREATE";
|
||||||
|
this.entityName = "INVESTMENTTRANSFER";
|
||||||
|
this.entityId = purchaseId;
|
||||||
|
this.href = "/investment/purchases/" + purchaseId + "/transfer";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public CommandWrapperBuilder postInvestmentInterest(final Long purchaseId) {
|
public CommandWrapperBuilder postInvestmentInterest(final Long purchaseId) {
|
||||||
this.actionName = "CREATE";
|
this.actionName = "CREATE";
|
||||||
this.entityName = "INVESTMENTINTEREST";
|
this.entityName = "INVESTMENTINTEREST";
|
||||||
|
|||||||
+83
@@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.api;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.ws.rs.Consumes;
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.POST;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.PathParam;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.QueryParam;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
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.InvestmentAmortisationData;
|
||||||
|
import org.apache.fineract.portfolio.investment.data.InvestmentAmortisationDueData;
|
||||||
|
import org.apache.fineract.portfolio.investment.service.InvestmentAmortisationService;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Path("/v1/investment/amortisation")
|
||||||
|
@Component
|
||||||
|
@Tag(name = "Investment Premium Amortisation", description = "Premium of HTM holdings amortised straight-line to maturity every 31 March.")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class InvestmentAmortisationApiResource {
|
||||||
|
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final InvestmentAmortisationService amortisationService;
|
||||||
|
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("{purchaseId}")
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Amortisation schedule of a holding (06 Amortize Entry)")
|
||||||
|
public InvestmentAmortisationData schedule(@PathParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(InvestmentSaleApiConstants.ENTITY);
|
||||||
|
return this.amortisationService.schedule(purchaseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("due")
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Holdings due for amortisation on a 31 March (07 Amortize Yearly Process)", description = "fyEnd dd-MM-yyyy")
|
||||||
|
public List<InvestmentAmortisationDueData> due(@QueryParam("fyEnd") @Parameter(description = "dd-MM-yyyy") final String fyEnd) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(InvestmentSaleApiConstants.ENTITY);
|
||||||
|
return this.amortisationService.due(LocalDate.parse(fyEnd.trim(), DateTimeFormatter.ofPattern("dd-MM-yyyy")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Post amortisation for a 31 March", description = "With purchaseId: that holding (Amortize Entry); without: "
|
||||||
|
+ "every holding due (Amortize Yearly Process). One JV per holding: Dr 500604 / Cr premium GL.")
|
||||||
|
public CommandProcessingResult amortise(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
final CommandWrapper request = new CommandWrapperBuilder().amortiseInvestmentPremium().withJson(apiRequestBodyAsJson).build();
|
||||||
|
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
@@ -35,6 +35,10 @@ public final class InvestmentPurchaseApiConstants {
|
|||||||
public static final String STATUS_PENDING_SETTLEMENT = "PENDING_SETTLEMENT";
|
public static final String STATUS_PENDING_SETTLEMENT = "PENDING_SETTLEMENT";
|
||||||
/** Settled: posted to GL on the settlement date, now a holding. */
|
/** Settled: posted to GL on the settlement date, now a holding. */
|
||||||
public static final String STATUS_ACTIVE = "ACTIVE";
|
public static final String STATUS_ACTIVE = "ACTIVE";
|
||||||
|
/** Every unit sold before maturity. */
|
||||||
|
public static final String STATUS_SOLD = "SOLD";
|
||||||
|
/** Repaid at maturity. */
|
||||||
|
public static final String STATUS_REDEEMED = "REDEEMED";
|
||||||
|
|
||||||
/** GLs the settlement entry debits besides the script's investment GL. */
|
/** GLs the settlement entry debits besides the script's investment GL. */
|
||||||
public static final String BROKEN_PERIOD_INTEREST_GL_CODE = "500606";
|
public static final String BROKEN_PERIOD_INTEREST_GL_CODE = "500606";
|
||||||
|
|||||||
+52
@@ -42,7 +42,9 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
|||||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
import org.apache.fineract.portfolio.investment.data.InvestmentInterestScheduleData;
|
import org.apache.fineract.portfolio.investment.data.InvestmentInterestScheduleData;
|
||||||
import org.apache.fineract.portfolio.investment.data.InvestmentPurchaseData;
|
import org.apache.fineract.portfolio.investment.data.InvestmentPurchaseData;
|
||||||
|
import org.apache.fineract.portfolio.investment.data.InvestmentSaleData;
|
||||||
import org.apache.fineract.portfolio.investment.service.InvestmentPurchaseReadPlatformService;
|
import org.apache.fineract.portfolio.investment.service.InvestmentPurchaseReadPlatformService;
|
||||||
|
import org.apache.fineract.portfolio.investment.service.InvestmentSaleReadPlatformService;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Path("/v1/investment/purchases")
|
@Path("/v1/investment/purchases")
|
||||||
@@ -53,6 +55,7 @@ public class InvestmentPurchaseApiResource {
|
|||||||
|
|
||||||
private final PlatformSecurityContext context;
|
private final PlatformSecurityContext context;
|
||||||
private final InvestmentPurchaseReadPlatformService readPlatformService;
|
private final InvestmentPurchaseReadPlatformService readPlatformService;
|
||||||
|
private final InvestmentSaleReadPlatformService saleReadPlatformService;
|
||||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@@ -121,6 +124,55 @@ public class InvestmentPurchaseApiResource {
|
|||||||
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("{purchaseId}/redemption-quote")
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "What the redemption at maturity will post", description = "Face value back against the book value still "
|
||||||
|
+ "held; refused before maturity or while a sale is pending.")
|
||||||
|
public InvestmentSaleData redemptionQuote(@PathParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(InvestmentSaleApiConstants.ENTITY);
|
||||||
|
return this.saleReadPlatformService.quoteRedemption(purchaseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("{purchaseId}/redeem")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Redeem at maturity", description = "On or after maturity, once every coupon is posted: posts the face value "
|
||||||
|
+ "received, dated the maturity date, and closes the holding.")
|
||||||
|
public CommandProcessingResult redeem(@PathParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId,
|
||||||
|
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
final CommandWrapper request = new CommandWrapperBuilder().redeemInvestmentPurchase(purchaseId)
|
||||||
|
.withJson(StringUtils.isBlank(apiRequestBodyAsJson) ? "{}" : apiRequestBodyAsJson).build();
|
||||||
|
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("{purchaseId}/write-off")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Write off part of the book value", description = "Dr Loss on Investments / Cr Investment for the amount "
|
||||||
|
+ "given; the units stay held at the reduced book value.")
|
||||||
|
public CommandProcessingResult writeOff(@PathParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId,
|
||||||
|
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
final CommandWrapper request = new CommandWrapperBuilder().writeOffInvestmentPurchase(purchaseId).withJson(apiRequestBodyAsJson)
|
||||||
|
.build();
|
||||||
|
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("{purchaseId}/transfer")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "GOI Category Transfer", description = "Moves the holding to another security (scriptId) of the same coupon "
|
||||||
|
+ "rate, maturity and unit value; the book value moves between GLs only when the two scripts use different GLs.")
|
||||||
|
public CommandProcessingResult transfer(@PathParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId,
|
||||||
|
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
final CommandWrapper request = new CommandWrapperBuilder().transferInvestmentSecurity(purchaseId).withJson(apiRequestBodyAsJson)
|
||||||
|
.build();
|
||||||
|
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||||
|
}
|
||||||
|
|
||||||
@DELETE
|
@DELETE
|
||||||
@Path("{purchaseId}")
|
@Path("{purchaseId}")
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
|||||||
+70
@@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
/** Sale (before maturity) and redemption (at maturity) of investment holdings. */
|
||||||
|
public final class InvestmentSaleApiConstants {
|
||||||
|
|
||||||
|
private InvestmentSaleApiConstants() {}
|
||||||
|
|
||||||
|
public static final String ENTITY = "INVESTMENTSALE";
|
||||||
|
public static final String RESOURCE = "investmentsale";
|
||||||
|
|
||||||
|
public static final String TYPE_SELL = "SELL";
|
||||||
|
public static final String TYPE_REDEEM = "REDEEM";
|
||||||
|
/** Part of the book value written off as a loss; the units stay held. */
|
||||||
|
public static final String TYPE_WRITEOFF = "WRITEOFF";
|
||||||
|
/** A year-end amortisation of premium (31 March); recorded like a sale so the book value comes down. */
|
||||||
|
public static final String TYPE_AMORTISE = "AMORTISE";
|
||||||
|
|
||||||
|
public static final String STATUS_PENDING_SETTLEMENT = "PENDING_SETTLEMENT";
|
||||||
|
public static final String STATUS_SETTLED = "SETTLED";
|
||||||
|
|
||||||
|
/** Profit on sale / redemption (income) and loss on sale / redemption (expense). */
|
||||||
|
public static final String PROFIT_ON_SALE_GL_CODE = "400405";
|
||||||
|
public static final String LOSS_ON_SALE_GL_CODE = "500605";
|
||||||
|
/** Premium still on the books at maturity is written off to the amortisation expense. */
|
||||||
|
public static final String PREMIUM_AMORTISATION_GL_CODE = "500604";
|
||||||
|
/** Premium paid over face value, held here until amortised (Jitendra Sir). */
|
||||||
|
public static final String PREMIUM_ON_INVESTMENTS_GL_CODE = "100707";
|
||||||
|
|
||||||
|
public static final String PURCHASE_ID = "purchaseId";
|
||||||
|
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 UNITS = "units";
|
||||||
|
public static final String SALE_RATE = "saleRate";
|
||||||
|
public static final String BROKERAGE = "brokerage";
|
||||||
|
public static final String BANK_GL_ACCOUNT_ID = "bankGlAccountId";
|
||||||
|
public static final String SANCTION_AUTHORITY = "sanctionAuthority";
|
||||||
|
public static final String REMARKS = "remarks";
|
||||||
|
public static final String AMOUNT = "amount";
|
||||||
|
public static final String FY_END = "fyEnd";
|
||||||
|
public static final String LOCALE = "locale";
|
||||||
|
public static final String DATE_FORMAT = "dateFormat";
|
||||||
|
|
||||||
|
public static final Set<String> SELL_PARAMS = Set.of(PURCHASE_ID, PARTY_ID, DEAL_DATE, DEAL_TIME, SETTLEMENT_TYPE, UNITS, SALE_RATE,
|
||||||
|
BROKERAGE, BANK_GL_ACCOUNT_ID, SANCTION_AUTHORITY, REMARKS, LOCALE, DATE_FORMAT);
|
||||||
|
public static final Set<String> REDEEM_PARAMS = Set.of(BANK_GL_ACCOUNT_ID, REMARKS, LOCALE, DATE_FORMAT);
|
||||||
|
public static final Set<String> WRITEOFF_PARAMS = Set.of(AMOUNT, REMARKS, LOCALE, DATE_FORMAT);
|
||||||
|
public static final Set<String> AMORTISE_PARAMS = Set.of(PURCHASE_ID, FY_END, LOCALE, DATE_FORMAT);
|
||||||
|
}
|
||||||
+116
@@ -0,0 +1,116 @@
|
|||||||
|
/**
|
||||||
|
* 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.QueryParam;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||||
|
import org.apache.fineract.commands.service.CommandWrapperBuilder;
|
||||||
|
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.investment.data.InvestmentSaleData;
|
||||||
|
import org.apache.fineract.portfolio.investment.service.InvestmentSaleReadPlatformService;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Path("/v1/investment/sales")
|
||||||
|
@Component
|
||||||
|
@Tag(name = "Investment Sale", description = "Sale of holdings before maturity (all or part of the units), settled on the settlement date.")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class InvestmentSaleApiResource {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
||||||
|
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final InvestmentSaleReadPlatformService readPlatformService;
|
||||||
|
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "List sales and redemptions, newest first")
|
||||||
|
public List<InvestmentSaleData> retrieveAll() {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(InvestmentSaleApiConstants.ENTITY);
|
||||||
|
return this.readPlatformService.retrieveAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("quote")
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Work out a sale before saving it", description = "Price, interest from the buyer, cost, profit or loss "
|
||||||
|
+ "and the net amount for the units, rate and deal date given (dd-MM-yyyy).")
|
||||||
|
public InvestmentSaleData quote(@QueryParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId,
|
||||||
|
@QueryParam("dealDate") @Parameter(description = "dd-MM-yyyy") final String dealDate,
|
||||||
|
@QueryParam("settlementType") @Parameter(description = "T+0 / T+1 / T+2") final String settlementType,
|
||||||
|
@QueryParam("units") @Parameter(description = "units") final BigDecimal units,
|
||||||
|
@QueryParam("saleRate") @Parameter(description = "saleRate") final BigDecimal saleRate,
|
||||||
|
@QueryParam("brokerage") @Parameter(description = "brokerage") final BigDecimal brokerage,
|
||||||
|
@QueryParam("bankGlAccountId") @Parameter(description = "bankGlAccountId") final Long bankGlAccountId) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(InvestmentSaleApiConstants.ENTITY);
|
||||||
|
final LocalDate deal = LocalDate.parse(dealDate.trim(), DATE);
|
||||||
|
return this.readPlatformService.quoteSale(purchaseId, deal, StringUtils.defaultIfBlank(settlementType, "T+0"), units, saleRate,
|
||||||
|
brokerage, bankGlAccountId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Save a sale", description = "Saved pending; nothing reaches the GL until it is settled.")
|
||||||
|
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
final CommandWrapper request = new CommandWrapperBuilder().createInvestmentSale().withJson(apiRequestBodyAsJson).build();
|
||||||
|
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("{saleId}/settle")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Settle a sale", description = "On or after the settlement date: posts the sale to GL dated the settlement "
|
||||||
|
+ "date and reduces the units held.")
|
||||||
|
public CommandProcessingResult settle(@PathParam("saleId") @Parameter(description = "saleId") final Long saleId,
|
||||||
|
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
final CommandWrapper request = new CommandWrapperBuilder().settleInvestmentSale(saleId)
|
||||||
|
.withJson(StringUtils.isBlank(apiRequestBodyAsJson) ? "{}" : apiRequestBodyAsJson).build();
|
||||||
|
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DELETE
|
||||||
|
@Path("{saleId}")
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Drop a pending sale")
|
||||||
|
public CommandProcessingResult delete(@PathParam("saleId") @Parameter(description = "saleId") final Long saleId) {
|
||||||
|
final CommandWrapper request = new CommandWrapperBuilder().deleteInvestmentSale(saleId).build();
|
||||||
|
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* 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.tags.Tag;
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.investment.data.InvestmentSecurityTransferData;
|
||||||
|
import org.apache.fineract.portfolio.investment.service.InvestmentSecurityTransferService;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Path("/v1/investment/transfers")
|
||||||
|
@Component
|
||||||
|
@Tag(name = "Investment Security Transfer", description = "GOI Category Transfer: a holding moved to another security of the same terms.")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class InvestmentTransferApiResource {
|
||||||
|
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final InvestmentSecurityTransferService transferService;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "List security transfers, newest first")
|
||||||
|
public List<InvestmentSecurityTransferData> retrieveAll() {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission("INVESTMENTTRANSFER");
|
||||||
|
return this.transferService.retrieveAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/** Premium amortisation schedule of an HTM holding: one line per 31 March to maturity, then the rest at maturity. */
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class InvestmentAmortisationData {
|
||||||
|
|
||||||
|
private final Long purchaseId;
|
||||||
|
private final String dealNo;
|
||||||
|
private final String certNo;
|
||||||
|
private final String scriptCode;
|
||||||
|
private final String scriptDesc;
|
||||||
|
private final String category;
|
||||||
|
private final String settlementDate;
|
||||||
|
private final String maturityDate;
|
||||||
|
private final BigDecimal faceValue;
|
||||||
|
private final BigDecimal premium;
|
||||||
|
private final BigDecimal unamortisedPremium;
|
||||||
|
private final BigDecimal bookValue;
|
||||||
|
private final String premiumGlCode;
|
||||||
|
private final List<Row> rows;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public static class Row {
|
||||||
|
|
||||||
|
/** YEAR (31 March) or MATURITY (the remainder, written off on redemption). */
|
||||||
|
private final String kind;
|
||||||
|
private final String fyEnd;
|
||||||
|
private final String fromDate;
|
||||||
|
private final String toDate;
|
||||||
|
private final long days;
|
||||||
|
private final BigDecimal amount;
|
||||||
|
private final BigDecimal premiumAfter;
|
||||||
|
private final boolean posted;
|
||||||
|
private final String voucherNo;
|
||||||
|
private final String glTransactionId;
|
||||||
|
/** Next line to post and its 31 March has come. */
|
||||||
|
private final boolean postable;
|
||||||
|
}
|
||||||
|
}
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/** A holding's premium amortisation due for a 31 March (Amortize Yearly Process). */
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class InvestmentAmortisationDueData {
|
||||||
|
|
||||||
|
private final Long purchaseId;
|
||||||
|
private final String dealNo;
|
||||||
|
private final String certNo;
|
||||||
|
private final String scriptCode;
|
||||||
|
private final String scriptDesc;
|
||||||
|
private final String fyEnd;
|
||||||
|
private final String fromDate;
|
||||||
|
private final long days;
|
||||||
|
private final BigDecimal unamortisedPremium;
|
||||||
|
private final BigDecimal amount;
|
||||||
|
}
|
||||||
+102
@@ -0,0 +1,102 @@
|
|||||||
|
/**
|
||||||
|
* 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.InvestmentSale;
|
||||||
|
|
||||||
|
/** A sale or redemption of a holding — also the shape of a quote before it is saved. */
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class InvestmentSaleData {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
||||||
|
|
||||||
|
private final Long id;
|
||||||
|
private final Long purchaseId;
|
||||||
|
private final String dealNo;
|
||||||
|
private final String certNo;
|
||||||
|
private final String bondType;
|
||||||
|
private final String scriptCode;
|
||||||
|
private final String scriptDesc;
|
||||||
|
private final String saleType;
|
||||||
|
private final Long partyId;
|
||||||
|
private final String partyName;
|
||||||
|
private final String dealDate;
|
||||||
|
private final String dealTime;
|
||||||
|
private final String settlementType;
|
||||||
|
private final String settlementDate;
|
||||||
|
private final BigDecimal units;
|
||||||
|
private final BigDecimal faceValue;
|
||||||
|
private final BigDecimal saleRate;
|
||||||
|
private final BigDecimal consideration;
|
||||||
|
private final Integer accruedDays;
|
||||||
|
private final BigDecimal accruedInterest;
|
||||||
|
private final BigDecimal receivableCleared;
|
||||||
|
/** Interest from the buyer not yet booked: accrued interest less the receivable cleared. */
|
||||||
|
private final BigDecimal interestIncome;
|
||||||
|
private final BigDecimal brokerage;
|
||||||
|
private final BigDecimal bookValueSold;
|
||||||
|
private final BigDecimal premiumCleared;
|
||||||
|
private final BigDecimal profitLoss;
|
||||||
|
private final BigDecimal netAmount;
|
||||||
|
private final Long bankGlAccountId;
|
||||||
|
private final String bankGlCode;
|
||||||
|
private final String bankGlName;
|
||||||
|
private final String investmentGlCode;
|
||||||
|
/** GL the premium share is credited to: 100707, or the investment GL for deals settled before it was used. */
|
||||||
|
private final String premiumGlCode;
|
||||||
|
private final String sanctionAuthority;
|
||||||
|
private final String remarks;
|
||||||
|
private final String status;
|
||||||
|
private final String glTransactionId;
|
||||||
|
private final String voucherNo;
|
||||||
|
private final String settledOnDate;
|
||||||
|
|
||||||
|
public static InvestmentSaleData from(final InvestmentSale s) {
|
||||||
|
return builder().id(s.getId()).purchaseId(s.getPurchase().getId()).dealNo(s.getPurchase().getDealNo())
|
||||||
|
.certNo(s.getPurchase().getCertNo()).bondType(s.getPurchase().getBondType())
|
||||||
|
.scriptCode(s.getPurchase().getScript().getScriptCode()).scriptDesc(s.getPurchase().getScript().getScriptDesc())
|
||||||
|
.saleType(s.getSaleType()).partyId(s.getParty() == null ? null : s.getParty().getId())
|
||||||
|
.partyName(s.getParty() == null ? null : s.getParty().getPartyName()).dealDate(format(s.getDealDate()))
|
||||||
|
.dealTime(s.getDealTime()).settlementType(s.getSettlementType()).settlementDate(format(s.getSettlementDate()))
|
||||||
|
.units(s.getUnits()).faceValue(s.getFaceValue()).saleRate(s.getSaleRate()).consideration(s.getConsideration())
|
||||||
|
.accruedDays(s.getAccruedDays()).accruedInterest(s.getAccruedInterest()).receivableCleared(s.getReceivableCleared())
|
||||||
|
.interestIncome(zero(s.getAccruedInterest()).subtract(zero(s.getReceivableCleared()))).brokerage(s.getBrokerage())
|
||||||
|
.bookValueSold(s.getBookValueSold()).premiumCleared(s.getPremiumCleared()).profitLoss(s.getProfitLoss()).netAmount(s.getNetAmount())
|
||||||
|
.bankGlAccountId(s.getBankGlAccount().getId()).bankGlCode(s.getBankGlAccount().getGlCode())
|
||||||
|
.bankGlName(s.getBankGlAccount().getName()).investmentGlCode(s.getPurchase().getInvestmentGlAccount().getGlCode())
|
||||||
|
.premiumGlCode(s.getPurchase().getPremiumGlAccount() != null ? s.getPurchase().getPremiumGlAccount().getGlCode()
|
||||||
|
: s.getPurchase().getInvestmentGlAccount().getGlCode())
|
||||||
|
.sanctionAuthority(s.getSanctionAuthority()).remarks(s.getRemarks()).status(s.getStatus())
|
||||||
|
.glTransactionId(s.getGlTransactionId()).voucherNo(s.getVoucherNo()).settledOnDate(format(s.getSettledOnDate())).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BigDecimal zero(final BigDecimal v) {
|
||||||
|
return v == null ? BigDecimal.ZERO : v;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String format(final LocalDate date) {
|
||||||
|
return date == null ? null : date.format(DATE_FORMAT);
|
||||||
|
}
|
||||||
|
}
|
||||||
+62
@@ -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 java.time.format.DateTimeFormatter;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentSecurityTransfer;
|
||||||
|
|
||||||
|
/** A GOI Category Transfer of a holding, for the transaction log. */
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class InvestmentSecurityTransferData {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
||||||
|
|
||||||
|
private final Long id;
|
||||||
|
private final Long purchaseId;
|
||||||
|
private final String dealNo;
|
||||||
|
private final String certNo;
|
||||||
|
private final String bondType;
|
||||||
|
private final String fromScriptCode;
|
||||||
|
private final String fromScriptDesc;
|
||||||
|
private final String toScriptCode;
|
||||||
|
private final String toScriptDesc;
|
||||||
|
private final String fromGlCode;
|
||||||
|
private final String toGlCode;
|
||||||
|
private final String toGlName;
|
||||||
|
private final String transferDate;
|
||||||
|
private final BigDecimal units;
|
||||||
|
private final BigDecimal bookValue;
|
||||||
|
private final String remarks;
|
||||||
|
private final String glTransactionId;
|
||||||
|
private final String voucherNo;
|
||||||
|
|
||||||
|
public static InvestmentSecurityTransferData from(final InvestmentSecurityTransfer t) {
|
||||||
|
return builder().id(t.getId()).purchaseId(t.getPurchase().getId()).dealNo(t.getPurchase().getDealNo())
|
||||||
|
.certNo(t.getPurchase().getCertNo()).bondType(t.getPurchase().getBondType()).fromScriptCode(t.getFromScript().getScriptCode())
|
||||||
|
.fromScriptDesc(t.getFromScript().getScriptDesc()).toScriptCode(t.getToScript().getScriptCode())
|
||||||
|
.toScriptDesc(t.getToScript().getScriptDesc()).fromGlCode(t.getFromGlAccount().getGlCode())
|
||||||
|
.toGlCode(t.getToGlAccount().getGlCode()).toGlName(t.getToGlAccount().getName())
|
||||||
|
.transferDate(t.getTransferDate().format(DATE_FORMAT)).units(t.getUnits()).bookValue(t.getBookValue())
|
||||||
|
.remarks(t.getRemarks()).glTransactionId(t.getGlTransactionId()).voucherNo(t.getVoucherNo()).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
@@ -143,6 +143,11 @@ public class InvestmentPurchase extends AbstractAuditableWithUTCDateTimeCustom<L
|
|||||||
@JoinColumn(name = "cr_gl_account_id", nullable = false)
|
@JoinColumn(name = "cr_gl_account_id", nullable = false)
|
||||||
private GLAccount crGlAccount;
|
private GLAccount crGlAccount;
|
||||||
|
|
||||||
|
/** GL holding the premium paid over face value (100707) until it is amortised; empty when it sits in the investment GL. */
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "premium_gl_account_id")
|
||||||
|
private GLAccount premiumGlAccount;
|
||||||
|
|
||||||
@Column(name = "other_details", length = 4000)
|
@Column(name = "other_details", length = 4000)
|
||||||
private String otherDetails;
|
private String otherDetails;
|
||||||
|
|
||||||
|
|||||||
+132
@@ -0,0 +1,132 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||||
|
|
||||||
|
/** A sale of some or all units of a holding before maturity (SELL), or its repayment at maturity (REDEEM). */
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_investment_sale")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class InvestmentSale extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "purchase_id", nullable = false)
|
||||||
|
private InvestmentPurchase purchase;
|
||||||
|
|
||||||
|
/** SELL, REDEEM, WRITEOFF or AMORTISE. */
|
||||||
|
@Column(name = "sale_type", nullable = false, length = 10)
|
||||||
|
private String saleType;
|
||||||
|
|
||||||
|
/** Buyer of a sale; empty for a redemption. */
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "party_id")
|
||||||
|
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", length = 5)
|
||||||
|
private String settlementType;
|
||||||
|
|
||||||
|
@Column(name = "settlement_date", nullable = false)
|
||||||
|
private LocalDate settlementDate;
|
||||||
|
|
||||||
|
@Column(name = "units", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal units;
|
||||||
|
|
||||||
|
@Column(name = "face_value", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal faceValue;
|
||||||
|
|
||||||
|
/** Price per ₹100 of face value; 100 for a redemption. */
|
||||||
|
@Column(name = "sale_rate", scale = 6, precision = 19)
|
||||||
|
private BigDecimal saleRate;
|
||||||
|
|
||||||
|
/** Price received for the units, without interest. */
|
||||||
|
@Column(name = "consideration", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal consideration;
|
||||||
|
|
||||||
|
@Column(name = "accrued_days")
|
||||||
|
private Integer accruedDays;
|
||||||
|
|
||||||
|
/** Interest the buyer pays us for the days since the last coupon. */
|
||||||
|
@Column(name = "accrued_interest", scale = 6, precision = 19)
|
||||||
|
private BigDecimal accruedInterest;
|
||||||
|
|
||||||
|
/** Part of that interest already booked by a closing provision, cleared from interest receivable. */
|
||||||
|
@Column(name = "receivable_cleared", scale = 6, precision = 19)
|
||||||
|
private BigDecimal receivableCleared;
|
||||||
|
|
||||||
|
@Column(name = "brokerage", scale = 6, precision = 19)
|
||||||
|
private BigDecimal brokerage;
|
||||||
|
|
||||||
|
/** Cost of the units sold, taken off the investment GL. */
|
||||||
|
@Column(name = "book_value_sold", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal bookValueSold;
|
||||||
|
|
||||||
|
/** Part of the book value taken off that was unamortised premium (credited to the premium GL). */
|
||||||
|
@Column(name = "premium_cleared", scale = 6, precision = 19)
|
||||||
|
private BigDecimal premiumCleared;
|
||||||
|
|
||||||
|
/** Consideration less cost: positive is profit, negative is loss. */
|
||||||
|
@Column(name = "profit_loss", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal profitLoss;
|
||||||
|
|
||||||
|
/** Money into the bank: consideration plus interest, less brokerage. */
|
||||||
|
@Column(name = "net_amount", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal netAmount;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "bank_gl_account_id", nullable = false)
|
||||||
|
private GLAccount bankGlAccount;
|
||||||
|
|
||||||
|
@Column(name = "sanction_authority", length = 100)
|
||||||
|
private String sanctionAuthority;
|
||||||
|
|
||||||
|
@Column(name = "remarks", length = 500)
|
||||||
|
private String remarks;
|
||||||
|
|
||||||
|
@Column(name = "status", nullable = false, length = 20)
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
@Column(name = "gl_transaction_id", length = 50)
|
||||||
|
private String glTransactionId;
|
||||||
|
|
||||||
|
@Column(name = "voucher_no", length = 30)
|
||||||
|
private String voucherNo;
|
||||||
|
|
||||||
|
@Column(name = "settled_on_date")
|
||||||
|
private LocalDate settledOnDate;
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.domain;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface InvestmentSaleRepository extends JpaRepository<InvestmentSale, Long> {
|
||||||
|
|
||||||
|
List<InvestmentSale> findByPurchaseIdOrderByIdAsc(Long purchaseId);
|
||||||
|
}
|
||||||
+5
-1
@@ -18,6 +18,10 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.portfolio.investment.domain;
|
package org.apache.fineract.portfolio.investment.domain;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
public interface InvestmentScriptRepository extends JpaRepository<InvestmentScript, Long> {}
|
public interface InvestmentScriptRepository extends JpaRepository<InvestmentScript, Long> {
|
||||||
|
|
||||||
|
List<InvestmentScript> findByIsinIgnoreCase(String isin);
|
||||||
|
}
|
||||||
|
|||||||
+80
@@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||||
|
|
||||||
|
/** A holding moved from one government security script to another of the same terms (GOI Category Transfer). */
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_investment_security_transfer")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class InvestmentSecurityTransfer extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "purchase_id", nullable = false)
|
||||||
|
private InvestmentPurchase purchase;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "from_script_id", nullable = false)
|
||||||
|
private InvestmentScript fromScript;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "to_script_id", nullable = false)
|
||||||
|
private InvestmentScript toScript;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "from_gl_account_id", nullable = false)
|
||||||
|
private GLAccount fromGlAccount;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "to_gl_account_id", nullable = false)
|
||||||
|
private GLAccount toGlAccount;
|
||||||
|
|
||||||
|
@Column(name = "transfer_date", nullable = false)
|
||||||
|
private LocalDate transferDate;
|
||||||
|
|
||||||
|
@Column(name = "units", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal units;
|
||||||
|
|
||||||
|
/** Book value carried over; moved between GLs only when the two scripts map to different GLs. */
|
||||||
|
@Column(name = "book_value", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal bookValue;
|
||||||
|
|
||||||
|
@Column(name = "remarks", length = 500)
|
||||||
|
private String remarks;
|
||||||
|
|
||||||
|
@Column(name = "gl_transaction_id", length = 50)
|
||||||
|
private String glTransactionId;
|
||||||
|
|
||||||
|
@Column(name = "voucher_no", length = 30)
|
||||||
|
private String voucherNo;
|
||||||
|
}
|
||||||
+23
@@ -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 InvestmentSecurityTransferRepository extends JpaRepository<InvestmentSecurityTransfer, Long> {}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.handler;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.commands.annotation.CommandType;
|
||||||
|
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||||
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.apache.fineract.portfolio.investment.service.InvestmentAmortisationService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@CommandType(entity = "INVESTMENTSALE", action = "AMORTISE")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AmortiseInvestmentPremiumCommandHandler implements NewCommandSourceHandler {
|
||||||
|
|
||||||
|
private final InvestmentAmortisationService amortisationService;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||||
|
return this.amortisationService.amortise(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.handler;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.commands.annotation.CommandType;
|
||||||
|
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||||
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.apache.fineract.portfolio.investment.service.InvestmentSaleWritePlatformService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@CommandType(entity = "INVESTMENTSALE", action = "CREATE")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CreateInvestmentSaleCommandHandler implements NewCommandSourceHandler {
|
||||||
|
|
||||||
|
private final InvestmentSaleWritePlatformService writePlatformService;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||||
|
return this.writePlatformService.createSale(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.handler;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.commands.annotation.CommandType;
|
||||||
|
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||||
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.apache.fineract.portfolio.investment.service.InvestmentSaleWritePlatformService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@CommandType(entity = "INVESTMENTSALE", action = "DELETE")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DeleteInvestmentSaleCommandHandler implements NewCommandSourceHandler {
|
||||||
|
|
||||||
|
private final InvestmentSaleWritePlatformService writePlatformService;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||||
|
return this.writePlatformService.deleteSale(command.entityId());
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.handler;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.commands.annotation.CommandType;
|
||||||
|
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||||
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.apache.fineract.portfolio.investment.service.InvestmentSaleWritePlatformService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@CommandType(entity = "INVESTMENTSALE", action = "REDEEM")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class RedeemInvestmentPurchaseCommandHandler implements NewCommandSourceHandler {
|
||||||
|
|
||||||
|
private final InvestmentSaleWritePlatformService writePlatformService;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||||
|
return this.writePlatformService.redeem(command.entityId(), command);
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.handler;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.commands.annotation.CommandType;
|
||||||
|
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||||
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.apache.fineract.portfolio.investment.service.InvestmentSaleWritePlatformService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@CommandType(entity = "INVESTMENTSALE", action = "SETTLE")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SettleInvestmentSaleCommandHandler implements NewCommandSourceHandler {
|
||||||
|
|
||||||
|
private final InvestmentSaleWritePlatformService writePlatformService;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||||
|
return this.writePlatformService.settleSale(command.entityId(), command);
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.handler;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.commands.annotation.CommandType;
|
||||||
|
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||||
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.apache.fineract.portfolio.investment.service.InvestmentSecurityTransferService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@CommandType(entity = "INVESTMENTTRANSFER", action = "CREATE")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class TransferInvestmentSecurityCommandHandler implements NewCommandSourceHandler {
|
||||||
|
|
||||||
|
private final InvestmentSecurityTransferService transferService;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||||
|
return this.transferService.transfer(command.entityId(), command);
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.handler;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.commands.annotation.CommandType;
|
||||||
|
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||||
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.apache.fineract.portfolio.investment.service.InvestmentSaleWritePlatformService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@CommandType(entity = "INVESTMENTSALE", action = "WRITEOFF")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class WriteOffInvestmentPurchaseCommandHandler implements NewCommandSourceHandler {
|
||||||
|
|
||||||
|
private final InvestmentSaleWritePlatformService writePlatformService;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||||
|
return this.writePlatformService.writeOff(command.entityId(), command);
|
||||||
|
}
|
||||||
|
}
|
||||||
+126
@@ -0,0 +1,126 @@
|
|||||||
|
/**
|
||||||
|
* 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.InvestmentSaleApiConstants.AMOUNT;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.BANK_GL_ACCOUNT_ID;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.BROKERAGE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.DEAL_DATE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.PARTY_ID;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.PURCHASE_ID;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.REDEEM_PARAMS;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.REMARKS;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.RESOURCE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.SALE_RATE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.SANCTION_AUTHORITY;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.SELL_PARAMS;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.SETTLEMENT_TYPE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.UNITS;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.WRITEOFF_PARAMS;
|
||||||
|
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
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.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/** Validates a sale entry and a redemption request. */
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public final class InvestmentSaleCommandFromApiJsonDeserializer {
|
||||||
|
|
||||||
|
private final FromJsonHelper fromApiJsonHelper;
|
||||||
|
|
||||||
|
public void validateForSell(final String json) {
|
||||||
|
final JsonElement element = parse(json, SELL_PARAMS);
|
||||||
|
final List<ApiParameterError> errors = new ArrayList<>();
|
||||||
|
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(RESOURCE);
|
||||||
|
v.reset().parameter(PURCHASE_ID).value(this.fromApiJsonHelper.extractLongNamed(PURCHASE_ID, element)).notNull()
|
||||||
|
.longGreaterThanZero();
|
||||||
|
v.reset().parameter(PARTY_ID).value(this.fromApiJsonHelper.extractLongNamed(PARTY_ID, element)).notNull().longGreaterThanZero();
|
||||||
|
v.reset().parameter(BANK_GL_ACCOUNT_ID).value(this.fromApiJsonHelper.extractLongNamed(BANK_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(InvestmentPurchaseApiConstants.SETTLEMENT_TYPES.toArray());
|
||||||
|
}
|
||||||
|
v.reset().parameter(UNITS).value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(UNITS, element)).notNull()
|
||||||
|
.positiveAmount();
|
||||||
|
v.reset().parameter(SALE_RATE).value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(SALE_RATE, element)).notNull()
|
||||||
|
.positiveAmount();
|
||||||
|
v.reset().parameter(BROKERAGE).value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(BROKERAGE, element)).ignoreIfNull()
|
||||||
|
.zeroOrPositiveAmount();
|
||||||
|
v.reset().parameter(SANCTION_AUTHORITY).value(this.fromApiJsonHelper.extractStringNamed(SANCTION_AUTHORITY, element))
|
||||||
|
.ignoreIfNull().notExceedingLengthOf(100);
|
||||||
|
v.reset().parameter(REMARKS).value(this.fromApiJsonHelper.extractStringNamed(REMARKS, element)).ignoreIfNull()
|
||||||
|
.notExceedingLengthOf(500);
|
||||||
|
throwIfAny(errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void validateForRedeem(final String json) {
|
||||||
|
final JsonElement element = parse(json, REDEEM_PARAMS);
|
||||||
|
final List<ApiParameterError> errors = new ArrayList<>();
|
||||||
|
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(RESOURCE);
|
||||||
|
v.reset().parameter(BANK_GL_ACCOUNT_ID).value(this.fromApiJsonHelper.extractLongNamed(BANK_GL_ACCOUNT_ID, element)).ignoreIfNull()
|
||||||
|
.longGreaterThanZero();
|
||||||
|
v.reset().parameter(REMARKS).value(this.fromApiJsonHelper.extractStringNamed(REMARKS, element)).ignoreIfNull()
|
||||||
|
.notExceedingLengthOf(500);
|
||||||
|
throwIfAny(errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void validateForWriteOff(final String json) {
|
||||||
|
final JsonElement element = parse(json, WRITEOFF_PARAMS);
|
||||||
|
final List<ApiParameterError> errors = new ArrayList<>();
|
||||||
|
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(RESOURCE);
|
||||||
|
v.reset().parameter(AMOUNT).value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(AMOUNT, element)).notNull()
|
||||||
|
.positiveAmount();
|
||||||
|
v.reset().parameter(REMARKS).value(this.fromApiJsonHelper.extractStringNamed(REMARKS, element)).notBlank()
|
||||||
|
.notExceedingLengthOf(500);
|
||||||
|
throwIfAny(errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonElement parse(final String json, final Set<String> params) {
|
||||||
|
if (StringUtils.isBlank(json)) {
|
||||||
|
throw new InvalidJsonException();
|
||||||
|
}
|
||||||
|
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
||||||
|
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, params);
|
||||||
|
return this.fromApiJsonHelper.parse(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void throwIfAny(final List<ApiParameterError> errors) {
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+292
@@ -0,0 +1,292 @@
|
|||||||
|
/**
|
||||||
|
* 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.STATUS_ACTIVE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.AMORTISE_PARAMS;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.FY_END;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.PREMIUM_AMORTISATION_GL_CODE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.PURCHASE_ID;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.STATUS_SETTLED;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.TYPE_AMORTISE;
|
||||||
|
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.Month;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||||
|
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
|
import org.apache.fineract.portfolio.investment.data.InvestmentAmortisationData;
|
||||||
|
import org.apache.fineract.portfolio.investment.data.InvestmentAmortisationDueData;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchase;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchaseRepository;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentSale;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentSaleRepository;
|
||||||
|
import org.apache.fineract.portfolio.investment.exception.InvestmentPurchaseNotFoundException;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Premium amortisation of HTM holdings: the premium paid over face value is written off straight-line, day by day, over
|
||||||
|
* the period remaining to maturity (RBI; method and 31 March timing confirmed with Jitendra Sir). Each 31 March posts the
|
||||||
|
* share for the days since the last amortisation (or settlement): unamortised premium x days to 31 March / days to
|
||||||
|
* maturity — so a part sale, which takes its share of premium with it, is followed automatically. The remainder after
|
||||||
|
* the last 31 March is written off with the redemption. Entry: Dr 500604 Amortisation of Premium / Cr Premium GL.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class InvestmentAmortisationService {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
||||||
|
private static final String HTM_PREFIX = "HTM";
|
||||||
|
|
||||||
|
private final FromJsonHelper fromApiJsonHelper;
|
||||||
|
private final InvestmentPurchaseRepository purchaseRepository;
|
||||||
|
private final InvestmentSaleRepository saleRepository;
|
||||||
|
private final InvestmentSaleQuoter quoter;
|
||||||
|
private final InvestmentJournalPoster journalPoster;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public InvestmentAmortisationData schedule(final Long purchaseId) {
|
||||||
|
final InvestmentPurchase p = purchase(purchaseId);
|
||||||
|
final List<InvestmentSale> posted = amortisations(p);
|
||||||
|
final LocalDate today = DateUtils.getBusinessLocalDate();
|
||||||
|
final List<InvestmentAmortisationData.Row> rows = new ArrayList<>();
|
||||||
|
BigDecimal premium = this.quoter.unamortisedPremium(p);
|
||||||
|
LocalDate start = p.getSettlementDate();
|
||||||
|
boolean nextFound = false;
|
||||||
|
for (final LocalDate fyEnd : yearEnds(p)) {
|
||||||
|
final Optional<InvestmentSale> done = posted.stream().filter(x -> x.getDealDate().equals(fyEnd)).findFirst();
|
||||||
|
final long days = ChronoUnit.DAYS.between(start, fyEnd);
|
||||||
|
if (done.isPresent()) {
|
||||||
|
rows.add(row("YEAR", fyEnd, start, days, done.get().getBookValueSold(), null, true, done.get(), false));
|
||||||
|
} else {
|
||||||
|
final BigDecimal amount = share(premium, start, fyEnd, p.getMaturityDate());
|
||||||
|
premium = premium.subtract(amount);
|
||||||
|
final boolean postable = STATUS_ACTIVE.equals(p.getStatus()) && !nextFound && !fyEnd.isAfter(today);
|
||||||
|
nextFound = true;
|
||||||
|
rows.add(row("YEAR", fyEnd, start, days, amount, premium, false, null, postable));
|
||||||
|
}
|
||||||
|
start = fyEnd;
|
||||||
|
}
|
||||||
|
if (premium.signum() > 0 && STATUS_ACTIVE.equals(p.getStatus())) {
|
||||||
|
rows.add(row("MATURITY", p.getMaturityDate(), start, ChronoUnit.DAYS.between(start, p.getMaturityDate()), premium,
|
||||||
|
BigDecimal.ZERO, false, null, false));
|
||||||
|
}
|
||||||
|
return InvestmentAmortisationData.builder().purchaseId(p.getId()).dealNo(p.getDealNo()).certNo(p.getCertNo())
|
||||||
|
.scriptCode(p.getScript().getScriptCode()).scriptDesc(p.getScript().getScriptDesc()).category(p.getCategory())
|
||||||
|
.settlementDate(p.getSettlementDate().format(DATE)).maturityDate(p.getMaturityDate().format(DATE))
|
||||||
|
.faceValue(p.getBalanceUnits().multiply(p.getScript().getFaceValuePerUnit()))
|
||||||
|
.premium(p.getPremiumDiscount() == null ? BigDecimal.ZERO : p.getPremiumDiscount().max(BigDecimal.ZERO))
|
||||||
|
.unamortisedPremium(this.quoter.unamortisedPremium(p)).bookValue(this.quoter.remainingBook(p))
|
||||||
|
.premiumGlCode(p.getPremiumGlAccount() != null ? p.getPremiumGlAccount().getGlCode() : p.getInvestmentGlAccount().getGlCode())
|
||||||
|
.rows(rows).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** HTM holdings with premium whose next amortisation falls on {@code fyEnd} (Amortize Yearly Process list). */
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<InvestmentAmortisationDueData> due(final LocalDate fyEnd) {
|
||||||
|
final List<InvestmentAmortisationDueData> due = new ArrayList<>();
|
||||||
|
for (final InvestmentPurchase p : this.purchaseRepository.findAll(Sort.by(Sort.Direction.ASC, "id"))) {
|
||||||
|
if (eligible(p) && fyEnd.equals(nextYearEnd(p))) {
|
||||||
|
final LocalDate start = periodStart(p);
|
||||||
|
final BigDecimal premium = this.quoter.unamortisedPremium(p);
|
||||||
|
due.add(InvestmentAmortisationDueData.builder().purchaseId(p.getId()).dealNo(p.getDealNo()).certNo(p.getCertNo())
|
||||||
|
.scriptCode(p.getScript().getScriptCode()).scriptDesc(p.getScript().getScriptDesc()).fyEnd(fyEnd.format(DATE))
|
||||||
|
.fromDate(start.format(DATE)).days(ChronoUnit.DAYS.between(start, fyEnd)).unamortisedPremium(premium)
|
||||||
|
.amount(share(premium, start, fyEnd, p.getMaturityDate())).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return due;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Posts the amortisation for {@code fyEnd} (a 31 March on or before today): for one holding when purchaseId is
|
||||||
|
* given (Amortize Entry), else for every holding due that year (Amortize Yearly Process). One JV per holding.
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public CommandProcessingResult amortise(final JsonCommand command) {
|
||||||
|
validate(command.json());
|
||||||
|
final LocalDate fyEnd = command.localDateValueOfParameterNamed(FY_END);
|
||||||
|
if (fyEnd.getMonth() != Month.MARCH || fyEnd.getDayOfMonth() != 31) {
|
||||||
|
throw InvestmentSaleQuoter.rule("amortise.not.yearend", "Amortisation is done as on 31 March");
|
||||||
|
}
|
||||||
|
if (fyEnd.isAfter(DateUtils.getBusinessLocalDate())) {
|
||||||
|
throw InvestmentSaleQuoter.rule("amortise.future", "Amortisation for " + fyEnd.format(DATE) + " can be done on or after that date");
|
||||||
|
}
|
||||||
|
final Long purchaseId = command.longValueOfParameterNamed(PURCHASE_ID);
|
||||||
|
final List<InvestmentPurchase> targets = new ArrayList<>();
|
||||||
|
if (purchaseId != null) {
|
||||||
|
final InvestmentPurchase p = purchase(purchaseId);
|
||||||
|
if (!eligible(p)) {
|
||||||
|
throw InvestmentSaleQuoter.rule("amortise.not.eligible",
|
||||||
|
"Deal " + p.getDealNo() + " has no premium left to amortise (only active HTM holdings bought above face value)");
|
||||||
|
}
|
||||||
|
final LocalDate next = nextYearEnd(p);
|
||||||
|
if (!fyEnd.equals(next)) {
|
||||||
|
throw InvestmentSaleQuoter.rule("amortise.out.of.order", next == null ? "Deal " + p.getDealNo() + " has no 31 March to amortise"
|
||||||
|
: "Amortise deal " + p.getDealNo() + " year by year: the next is " + next.format(DATE));
|
||||||
|
}
|
||||||
|
targets.add(p);
|
||||||
|
} else {
|
||||||
|
for (final InvestmentPurchase p : this.purchaseRepository.findAll(Sort.by(Sort.Direction.ASC, "id"))) {
|
||||||
|
if (eligible(p) && fyEnd.equals(nextYearEnd(p))) {
|
||||||
|
targets.add(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (targets.isEmpty()) {
|
||||||
|
throw InvestmentSaleQuoter.rule("amortise.nothing.due", "No holding has premium amortisation due for " + fyEnd.format(DATE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final List<String> vouchers = new ArrayList<>();
|
||||||
|
Long lastId = null;
|
||||||
|
for (final InvestmentPurchase p : targets) {
|
||||||
|
final InvestmentSale s = post(p, fyEnd);
|
||||||
|
vouchers.add(s.getVoucherNo());
|
||||||
|
lastId = s.getId();
|
||||||
|
}
|
||||||
|
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(lastId)
|
||||||
|
.withResourceIdAsString(String.join(", ", vouchers)).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private InvestmentSale post(final InvestmentPurchase p, final LocalDate fyEnd) {
|
||||||
|
final LocalDate start = periodStart(p);
|
||||||
|
final BigDecimal amount = share(this.quoter.unamortisedPremium(p), start, fyEnd, p.getMaturityDate());
|
||||||
|
final String fy = (fyEnd.getYear() - 1) + "-" + String.format("%02d", fyEnd.getYear() % 100);
|
||||||
|
final CommandProcessingResult posted = this.journalPoster.post(fyEnd, p.getDealNo(),
|
||||||
|
"Premium amortisation FY " + fy + ": deal " + p.getDealNo() + ", " + p.getScript().getScriptCode() + " "
|
||||||
|
+ p.getScript().getScriptDesc() + " (" + ChronoUnit.DAYS.between(start, fyEnd) + " days)",
|
||||||
|
new InvestmentJournalPoster.Lines().debit(this.journalPoster.glId(PREMIUM_AMORTISATION_GL_CODE), amount)
|
||||||
|
.credit(InvestmentSaleQuoter.premiumGlId(p), amount));
|
||||||
|
final InvestmentSale s = new InvestmentSale();
|
||||||
|
s.setPurchase(p);
|
||||||
|
s.setSaleType(TYPE_AMORTISE);
|
||||||
|
s.setDealDate(fyEnd);
|
||||||
|
s.setSettlementDate(fyEnd);
|
||||||
|
s.setUnits(BigDecimal.ZERO);
|
||||||
|
s.setFaceValue(BigDecimal.ZERO);
|
||||||
|
s.setConsideration(BigDecimal.ZERO);
|
||||||
|
s.setAccruedDays((int) ChronoUnit.DAYS.between(start, fyEnd));
|
||||||
|
s.setAccruedInterest(BigDecimal.ZERO);
|
||||||
|
s.setReceivableCleared(BigDecimal.ZERO);
|
||||||
|
s.setBrokerage(BigDecimal.ZERO);
|
||||||
|
s.setBookValueSold(amount);
|
||||||
|
s.setPremiumCleared(amount);
|
||||||
|
s.setProfitLoss(amount.negate());
|
||||||
|
s.setNetAmount(BigDecimal.ZERO);
|
||||||
|
s.setBankGlAccount(p.getCrGlAccount());
|
||||||
|
s.setRemarks("Premium amortisation FY " + fy);
|
||||||
|
s.setStatus(STATUS_SETTLED);
|
||||||
|
s.setGlTransactionId(posted.getTransactionId());
|
||||||
|
final Object voucherNo = posted.getChanges() == null ? null : posted.getChanges().get("voucherNo");
|
||||||
|
s.setVoucherNo(voucherNo == null ? null : voucherNo.toString());
|
||||||
|
s.setSettledOnDate(DateUtils.getBusinessLocalDate());
|
||||||
|
return this.saleRepository.saveAndFlush(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Straight-line by days: the share of the unamortised premium for start..yearEnd out of start..maturity. */
|
||||||
|
static BigDecimal share(final BigDecimal premium, final LocalDate start, final LocalDate yearEnd, final LocalDate maturity) {
|
||||||
|
final long total = ChronoUnit.DAYS.between(start, maturity);
|
||||||
|
if (premium.signum() <= 0 || total <= 0) {
|
||||||
|
return BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
return premium.multiply(BigDecimal.valueOf(ChronoUnit.DAYS.between(start, yearEnd))).divide(BigDecimal.valueOf(total), 2,
|
||||||
|
RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean eligible(final InvestmentPurchase p) {
|
||||||
|
return STATUS_ACTIVE.equals(p.getStatus()) && p.getCategory() != null && p.getCategory().startsWith(HTM_PREFIX)
|
||||||
|
&& this.quoter.unamortisedPremium(p).signum() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The first 31 March after the last amortisation (or the settlement) that is still before maturity. */
|
||||||
|
private LocalDate nextYearEnd(final InvestmentPurchase p) {
|
||||||
|
final LocalDate start = periodStart(p);
|
||||||
|
return yearEnds(p).stream().filter(d -> d.isAfter(start)).findFirst().orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LocalDate periodStart(final InvestmentPurchase p) {
|
||||||
|
return amortisations(p).stream().map(InvestmentSale::getDealDate).max(LocalDate::compareTo).orElse(p.getSettlementDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 31 March dates after the settlement and before maturity. */
|
||||||
|
private static List<LocalDate> yearEnds(final InvestmentPurchase p) {
|
||||||
|
final List<LocalDate> ends = new ArrayList<>();
|
||||||
|
for (int y = p.getSettlementDate().getYear(); y <= p.getMaturityDate().getYear(); y++) {
|
||||||
|
final LocalDate d = LocalDate.of(y, Month.MARCH, 31);
|
||||||
|
if (d.isAfter(p.getSettlementDate()) && d.isBefore(p.getMaturityDate())) {
|
||||||
|
ends.add(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ends;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<InvestmentSale> amortisations(final InvestmentPurchase p) {
|
||||||
|
return this.saleRepository.findByPurchaseIdOrderByIdAsc(p.getId()).stream()
|
||||||
|
.filter(s -> TYPE_AMORTISE.equals(s.getSaleType()) && STATUS_SETTLED.equals(s.getStatus())).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InvestmentAmortisationData.Row row(final String kind, final LocalDate end, final LocalDate start, final long days,
|
||||||
|
final BigDecimal amount, final BigDecimal after, final boolean posted, final InvestmentSale done, final boolean postable) {
|
||||||
|
return InvestmentAmortisationData.Row.builder().kind(kind).fyEnd(end.format(DATE)).fromDate(start.format(DATE))
|
||||||
|
.toDate(end.format(DATE)).days(days).amount(amount).premiumAfter(after).posted(posted)
|
||||||
|
.voucherNo(done == null ? null : done.getVoucherNo()).glTransactionId(done == null ? null : done.getGlTransactionId())
|
||||||
|
.postable(postable).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private InvestmentPurchase purchase(final Long purchaseId) {
|
||||||
|
return this.purchaseRepository.findById(purchaseId).orElseThrow(() -> new InvestmentPurchaseNotFoundException(purchaseId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validate(final String json) {
|
||||||
|
if (StringUtils.isBlank(json)) {
|
||||||
|
throw new InvalidJsonException();
|
||||||
|
}
|
||||||
|
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
||||||
|
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, AMORTISE_PARAMS);
|
||||||
|
final JsonElement element = this.fromApiJsonHelper.parse(json);
|
||||||
|
final List<ApiParameterError> errors = new ArrayList<>();
|
||||||
|
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource("investmentsale");
|
||||||
|
v.reset().parameter(FY_END).value(this.fromApiJsonHelper.extractLocalDateNamed(FY_END, element)).notNull();
|
||||||
|
v.reset().parameter(PURCHASE_ID).value(this.fromApiJsonHelper.extractLongNamed(PURCHASE_ID, element)).ignoreIfNull()
|
||||||
|
.longGreaterThanZero();
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
@@ -211,6 +211,9 @@ public class InvestmentMasterWritePlatformServiceImpl implements InvestmentMaste
|
|||||||
private void applyScript(final InvestmentScript s, final JsonCommand command) {
|
private void applyScript(final InvestmentScript s, final JsonCommand command) {
|
||||||
setIfPresent(command, SLR_TYPE, s::setSlrType);
|
setIfPresent(command, SLR_TYPE, s::setSlrType);
|
||||||
setIfPresent(command, SCRIPT_DESC, s::setScriptDesc);
|
setIfPresent(command, SCRIPT_DESC, s::setScriptDesc);
|
||||||
|
if (command.parameterExists(ISIN)) {
|
||||||
|
requireUniqueIsin(s, command.stringValueOfParameterNamed(ISIN));
|
||||||
|
}
|
||||||
setIfPresent(command, ISIN, s::setIsin);
|
setIfPresent(command, ISIN, s::setIsin);
|
||||||
setIfPresent(command, COMPANY_NAME, s::setCompanyName);
|
setIfPresent(command, COMPANY_NAME, s::setCompanyName);
|
||||||
setIfPresent(command, ADDRESS1, s::setAddress1);
|
setIfPresent(command, ADDRESS1, s::setAddress1);
|
||||||
@@ -242,6 +245,19 @@ public class InvestmentMasterWritePlatformServiceImpl implements InvestmentMaste
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** An ISIN identifies one security, so two scripts cannot carry it; checked when it is entered or changed. */
|
||||||
|
private void requireUniqueIsin(final InvestmentScript s, final String isin) {
|
||||||
|
if (StringUtils.isBlank(isin) || isin.trim().equalsIgnoreCase(StringUtils.trimToEmpty(s.getIsin()))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.scriptRepository.findByIsinIgnoreCase(isin.trim()).stream().filter(other -> !other.getId().equals(s.getId())).findFirst()
|
||||||
|
.ifPresent(other -> {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.investmentscript.isin.duplicate",
|
||||||
|
"ISIN " + isin.trim() + " is already used by script " + other.getScriptCode() + " " + other.getScriptDesc(),
|
||||||
|
isin.trim());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private InvestmentScript saveScript(final InvestmentScript script) {
|
private InvestmentScript saveScript(final InvestmentScript script) {
|
||||||
try {
|
try {
|
||||||
return this.scriptRepository.saveAndFlush(script);
|
return this.scriptRepository.saveAndFlush(script);
|
||||||
|
|||||||
+6
-2
@@ -87,8 +87,12 @@ public class InvestmentPurchaseReadPlatformService {
|
|||||||
nextFound = true;
|
nextFound = true;
|
||||||
}
|
}
|
||||||
rows.add(InvestmentInterestScheduleData.Row.builder().type(r.type()).intDate(format(r.date())).fromDate(format(r.fromDate()))
|
rows.add(InvestmentInterestScheduleData.Row.builder().type(r.type()).intDate(format(r.date())).fromDate(format(r.fromDate()))
|
||||||
.toDate(format(r.toDate())).days(r.days()).grossInterest(r.grossInterest()).alreadyBooked(r.alreadyBooked())
|
.toDate(format(r.toDate())).days(r.days())
|
||||||
.amount(r.amount()).due(!r.date().isAfter(asOn)).posted(done.isPresent())
|
// A posted line shows what was posted, even after a part sale has changed the face value held.
|
||||||
|
.grossInterest(done.map(InvestmentInterestPosting::getGrossInterest).orElse(r.grossInterest()))
|
||||||
|
.alreadyBooked(done.map(InvestmentInterestPosting::getAlreadyBooked).orElse(r.alreadyBooked()))
|
||||||
|
.amount(done.map(InvestmentInterestPosting::getAmount).orElse(r.amount())).due(!r.date().isAfter(asOn))
|
||||||
|
.posted(done.isPresent())
|
||||||
.voucherNo(done.map(InvestmentInterestPosting::getVoucherNo).orElse(null))
|
.voucherNo(done.map(InvestmentInterestPosting::getVoucherNo).orElse(null))
|
||||||
.glTransactionId(done.map(InvestmentInterestPosting::getGlTransactionId).orElse(null))
|
.glTransactionId(done.map(InvestmentInterestPosting::getGlTransactionId).orElse(null))
|
||||||
.postedOnDate(done.map(x -> format(x.getPostedOnDate())).orElse(null)).postable(isNext && !r.date().isAfter(today))
|
.postedOnDate(done.map(x -> format(x.getPostedOnDate())).orElse(null)).postable(isNext && !r.date().isAfter(today))
|
||||||
|
|||||||
+9
-1
@@ -193,8 +193,16 @@ public class InvestmentPurchaseWritePlatformServiceImpl implements InvestmentPur
|
|||||||
+ p.getSettlementDate().format(DateTimeFormatter.ofPattern("dd-MM-yyyy")));
|
+ p.getSettlementDate().format(DateTimeFormatter.ofPattern("dd-MM-yyyy")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Premium over face value goes to its own GL (100707) and is amortised to maturity; the investment GL takes the rest.
|
||||||
|
final BigDecimal premium = p.getPremiumDiscount() != null && p.getPremiumDiscount().signum() > 0 ? p.getPremiumDiscount()
|
||||||
|
: BigDecimal.ZERO;
|
||||||
|
if (premium.signum() > 0) {
|
||||||
|
p.setPremiumGlAccount(this.glAccountRepository.findOneByGlCodeWithNotFoundDetection(
|
||||||
|
org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.PREMIUM_ON_INVESTMENTS_GL_CODE));
|
||||||
|
}
|
||||||
final InvestmentJournalPoster.Lines lines = new InvestmentJournalPoster.Lines()
|
final InvestmentJournalPoster.Lines lines = new InvestmentJournalPoster.Lines()
|
||||||
.debit(p.getInvestmentGlAccount().getId(), p.getBookValue())
|
.debit(p.getInvestmentGlAccount().getId(), p.getBookValue().subtract(premium))
|
||||||
|
.debit(premium.signum() > 0 ? p.getPremiumGlAccount().getId() : null, premium)
|
||||||
.debit(this.journalPoster.glId(BROKEN_PERIOD_INTEREST_GL_CODE), p.getAccruedInterest())
|
.debit(this.journalPoster.glId(BROKEN_PERIOD_INTEREST_GL_CODE), p.getAccruedInterest())
|
||||||
.debit(this.journalPoster.glId(STAMP_DUTY_GL_CODE), p.getStampDuty())
|
.debit(this.journalPoster.glId(STAMP_DUTY_GL_CODE), p.getStampDuty())
|
||||||
.debit(this.journalPoster.glId(BROKERAGE_GL_CODE), p.getBrokerage())
|
.debit(this.journalPoster.glId(BROKERAGE_GL_CODE), p.getBrokerage())
|
||||||
|
|||||||
+294
@@ -0,0 +1,294 @@
|
|||||||
|
/**
|
||||||
|
* 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.STATUS_ACTIVE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.STATUS_PENDING_SETTLEMENT;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.STATUS_SETTLED;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.TYPE_REDEEM;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.TYPE_SELL;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.fineract.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.exception.GeneralPlatformDomainRuleException;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
|
import org.apache.fineract.portfolio.investment.api.InvestmentApiConstants;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentInterestPosting;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentInterestPostingRepository;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentParty;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchase;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentSale;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentSaleRepository;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Works out a sale or a redemption of a holding — the same figures for the screen's preview, for saving and for
|
||||||
|
* posting, so the voucher always matches what was shown.
|
||||||
|
* <ul>
|
||||||
|
* <li>Sale: price = face value sold × rate / 100; cost = the units' share of the book value; profit or loss = price −
|
||||||
|
* cost. The voucher carries the principal only (Jitendra Sir): the interest since the last coupon that the buyer pays is
|
||||||
|
* worked out as a hint — the part a closing provision already booked as receivable and the rest as income — for a manual
|
||||||
|
* entry. No short sale: only units still held and not in another pending sale.</li>
|
||||||
|
* <li>Redemption: the face value comes back at maturity against the book value still held; a discount left is profit,
|
||||||
|
* a premium left is written off.</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class InvestmentSaleQuoter {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
||||||
|
private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);
|
||||||
|
private static final String HTM_PREFIX = "HTM";
|
||||||
|
|
||||||
|
private final InvestmentSaleRepository saleRepository;
|
||||||
|
private final InvestmentInterestPostingRepository postingRepository;
|
||||||
|
private final InvestmentPurchaseReadPlatformService purchaseReadPlatformService;
|
||||||
|
private final GLAccountRepositoryWrapper glAccountRepository;
|
||||||
|
|
||||||
|
/** Inputs of a sale entry. */
|
||||||
|
public record SellRequest(InvestmentParty party, LocalDate dealDate, String dealTime, String settlementType, BigDecimal units,
|
||||||
|
BigDecimal saleRate, BigDecimal brokerage, Long bankGlAccountId, String sanctionAuthority, String remarks) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A pending sale with its figures worked out; {@code ignoreSaleId} leaves the sale being settled out of the units
|
||||||
|
* check. A {@code preview} skips the checks on what the user has not typed yet (buyer, HTM sanction).
|
||||||
|
*/
|
||||||
|
public InvestmentSale quoteSale(final InvestmentPurchase p, final SellRequest r, final Long ignoreSaleId, final boolean preview) {
|
||||||
|
requireActiveDebtHolding(p);
|
||||||
|
if (r.party() != null && !r.party().isActive()) {
|
||||||
|
throw rule("party.inactive", "Party " + r.party().getPartyCode() + " is inactive");
|
||||||
|
}
|
||||||
|
if (r.dealDate().isBefore(p.getSettlementDate())) {
|
||||||
|
throw rule("deal.before.purchase", "A sale cannot be dated before the purchase settled on " + p.getSettlementDate().format(DATE));
|
||||||
|
}
|
||||||
|
if (r.dealDate().isAfter(DateUtils.getBusinessLocalDate())) {
|
||||||
|
throw rule("deal.future", "The sale deal date cannot be after today");
|
||||||
|
}
|
||||||
|
final LocalDate settlementDate = r.dealDate().plusDays(Long.parseLong(r.settlementType().substring(2)));
|
||||||
|
if (!settlementDate.isBefore(p.getMaturityDate())) {
|
||||||
|
throw rule("sell.at.maturity", "The security matures on " + p.getMaturityDate().format(DATE)
|
||||||
|
+ "; on or after maturity it is redeemed, not sold");
|
||||||
|
}
|
||||||
|
if (!preview && p.getCategory() != null && p.getCategory().startsWith(HTM_PREFIX)
|
||||||
|
&& (StringUtils.isBlank(r.sanctionAuthority()) || StringUtils.isBlank(r.remarks()))) {
|
||||||
|
throw rule("htm.sale.sanction", "An HTM security may be sold only with the Board's permission: enter the sanction "
|
||||||
|
+ "authority and the reason for the sale in Remarks");
|
||||||
|
}
|
||||||
|
final BigDecimal available = availableUnits(p, ignoreSaleId);
|
||||||
|
if (r.units().compareTo(available) > 0) {
|
||||||
|
throw rule("units.exceed", "Only " + available.stripTrailingZeros().toPlainString() + " units of deal " + p.getDealNo()
|
||||||
|
+ " are available to sell");
|
||||||
|
}
|
||||||
|
|
||||||
|
final BigDecimal unitValue = p.getScript().getFaceValuePerUnit();
|
||||||
|
final BigDecimal face = r.units().multiply(unitValue).setScale(2, RoundingMode.HALF_UP);
|
||||||
|
final BigDecimal consideration = face.multiply(r.saleRate()).divide(HUNDRED, 2, RoundingMode.HALF_UP);
|
||||||
|
final LocalDate lastCoupon = lastCoupon(p, settlementDate);
|
||||||
|
final int days = InvestmentInterestCalculator.days360(lastCoupon, settlementDate);
|
||||||
|
final BigDecimal rate = p.getIntRate() == null ? BigDecimal.ZERO : p.getIntRate();
|
||||||
|
final BigDecimal accrued = InvestmentInterestCalculator.interestForDays(face, rate, days);
|
||||||
|
final BigDecimal receivable = receivableShare(p, lastCoupon, settlementDate, r.units()).min(accrued);
|
||||||
|
final boolean allRemaining = r.units().compareTo(p.getBalanceUnits()) == 0;
|
||||||
|
// Cost is the units' share of the book value as it stands now (after earlier sales and write-offs).
|
||||||
|
final BigDecimal book = remainingBook(p);
|
||||||
|
final BigDecimal bookSold = allRemaining ? book : book.multiply(r.units()).divide(p.getBalanceUnits(), 2, RoundingMode.HALF_UP);
|
||||||
|
final BigDecimal brokerage = r.brokerage() == null ? BigDecimal.ZERO : r.brokerage();
|
||||||
|
|
||||||
|
final InvestmentSale s = new InvestmentSale();
|
||||||
|
s.setPurchase(p);
|
||||||
|
s.setSaleType(TYPE_SELL);
|
||||||
|
s.setParty(r.party());
|
||||||
|
s.setDealDate(r.dealDate());
|
||||||
|
s.setDealTime(r.dealTime());
|
||||||
|
s.setSettlementType(r.settlementType());
|
||||||
|
s.setSettlementDate(settlementDate);
|
||||||
|
s.setUnits(r.units());
|
||||||
|
s.setFaceValue(face);
|
||||||
|
s.setSaleRate(r.saleRate());
|
||||||
|
s.setConsideration(consideration);
|
||||||
|
s.setAccruedDays(days);
|
||||||
|
s.setAccruedInterest(accrued);
|
||||||
|
s.setReceivableCleared(receivable);
|
||||||
|
s.setBrokerage(brokerage);
|
||||||
|
s.setBookValueSold(bookSold);
|
||||||
|
final BigDecimal premium = unamortisedPremium(p);
|
||||||
|
s.setPremiumCleared(allRemaining ? premium : premium.multiply(r.units()).divide(p.getBalanceUnits(), 2, RoundingMode.HALF_UP));
|
||||||
|
s.setProfitLoss(consideration.subtract(bookSold));
|
||||||
|
// Jitendra Sir: the sale voucher carries the principal only; the interest is entered by hand (hint above).
|
||||||
|
s.setNetAmount(consideration.subtract(brokerage));
|
||||||
|
s.setBankGlAccount(r.bankGlAccountId() == null ? p.getCrGlAccount() : postableAssetGl(r.bankGlAccountId()));
|
||||||
|
s.setSanctionAuthority(StringUtils.trimToNull(r.sanctionAuthority()));
|
||||||
|
s.setRemarks(StringUtils.trimToNull(r.remarks()));
|
||||||
|
s.setStatus(STATUS_PENDING_SETTLEMENT);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Repayment at maturity of all units still held, dated the maturity date. */
|
||||||
|
public InvestmentSale quoteRedemption(final InvestmentPurchase p, final Long bankGlAccountId, final String remarks) {
|
||||||
|
requireActiveDebtHolding(p);
|
||||||
|
if (p.getMaturityDate().isAfter(DateUtils.getBusinessLocalDate())) {
|
||||||
|
throw rule("redeem.before.maturity", "Deal " + p.getDealNo() + " matures on " + p.getMaturityDate().format(DATE)
|
||||||
|
+ "; it can be redeemed on or after that date");
|
||||||
|
}
|
||||||
|
if (availableUnits(p, null).compareTo(p.getBalanceUnits()) != 0) {
|
||||||
|
throw rule("redeem.pending.sale", "Deal " + p.getDealNo() + " has a pending sale; settle or drop it before redemption");
|
||||||
|
}
|
||||||
|
final BigDecimal face = p.getBalanceUnits().multiply(p.getScript().getFaceValuePerUnit()).setScale(2, RoundingMode.HALF_UP);
|
||||||
|
final BigDecimal book = remainingBook(p);
|
||||||
|
|
||||||
|
final InvestmentSale s = new InvestmentSale();
|
||||||
|
s.setPurchase(p);
|
||||||
|
s.setSaleType(TYPE_REDEEM);
|
||||||
|
s.setDealDate(p.getMaturityDate());
|
||||||
|
s.setSettlementDate(p.getMaturityDate());
|
||||||
|
s.setUnits(p.getBalanceUnits());
|
||||||
|
s.setFaceValue(face);
|
||||||
|
s.setSaleRate(HUNDRED);
|
||||||
|
s.setConsideration(face);
|
||||||
|
s.setAccruedDays(0);
|
||||||
|
s.setAccruedInterest(BigDecimal.ZERO);
|
||||||
|
s.setReceivableCleared(BigDecimal.ZERO);
|
||||||
|
s.setBrokerage(BigDecimal.ZERO);
|
||||||
|
s.setBookValueSold(book);
|
||||||
|
s.setPremiumCleared(unamortisedPremium(p));
|
||||||
|
s.setProfitLoss(face.subtract(book));
|
||||||
|
s.setNetAmount(face);
|
||||||
|
s.setBankGlAccount(bankGlAccountId == null ? p.getCrGlAccount() : postableAssetGl(bankGlAccountId));
|
||||||
|
s.setRemarks(StringUtils.trimToNull(remarks));
|
||||||
|
s.setStatus(STATUS_SETTLED);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every interest line up to {@code upTo} must be posted first, so the receivable cleared on a sale and the last
|
||||||
|
* coupon on a redemption are already in the books.
|
||||||
|
*/
|
||||||
|
public void requireInterestPostedUpTo(final InvestmentPurchase p, final LocalDate upTo) {
|
||||||
|
final List<InvestmentInterestPosting> posted = this.postingRepository.findByPurchaseIdOrderByIntDateAsc(p.getId());
|
||||||
|
this.purchaseReadPlatformService.scheduleRows(p).stream().filter(r -> !r.date().isAfter(upTo))
|
||||||
|
.filter(r -> posted.stream().noneMatch(x -> x.getLineType().equals(r.type()) && x.getIntDate().equals(r.date())))
|
||||||
|
.findFirst().ifPresent(r -> {
|
||||||
|
throw rule("interest.pending", "Post the " + r.type().toLowerCase() + " of " + r.date().format(DATE) + " for deal "
|
||||||
|
+ p.getDealNo() + " first (Interest Entry)");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Units still held less those in other pending sales. */
|
||||||
|
public BigDecimal availableUnits(final InvestmentPurchase p, final Long ignoreSaleId) {
|
||||||
|
BigDecimal available = p.getBalanceUnits();
|
||||||
|
for (final InvestmentSale s : this.saleRepository.findByPurchaseIdOrderByIdAsc(p.getId())) {
|
||||||
|
if (STATUS_PENDING_SETTLEMENT.equals(s.getStatus()) && !s.getId().equals(ignoreSaleId)) {
|
||||||
|
available = available.subtract(s.getUnits());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return available;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Premium paid over face value not yet taken off by amortisation, sales or the redemption. */
|
||||||
|
public BigDecimal unamortisedPremium(final InvestmentPurchase p) {
|
||||||
|
if (p.getPremiumDiscount() == null || p.getPremiumDiscount().signum() <= 0) {
|
||||||
|
return BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
BigDecimal premium = p.getPremiumDiscount();
|
||||||
|
for (final InvestmentSale s : this.saleRepository.findByPurchaseIdOrderByIdAsc(p.getId())) {
|
||||||
|
if (STATUS_SETTLED.equals(s.getStatus()) && s.getPremiumCleared() != null) {
|
||||||
|
premium = premium.subtract(s.getPremiumCleared());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return premium.max(BigDecimal.ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GL the unamortised premium sits in: 100707 for deals settled since it was introduced, else the investment GL. */
|
||||||
|
public static Long premiumGlId(final InvestmentPurchase p) {
|
||||||
|
return p.getPremiumGlAccount() != null ? p.getPremiumGlAccount().getId() : p.getInvestmentGlAccount().getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Book value not yet taken off by settled sales. */
|
||||||
|
public BigDecimal remainingBook(final InvestmentPurchase p) {
|
||||||
|
BigDecimal book = p.getBookValue();
|
||||||
|
for (final InvestmentSale s : this.saleRepository.findByPurchaseIdOrderByIdAsc(p.getId())) {
|
||||||
|
if (STATUS_SETTLED.equals(s.getStatus())) {
|
||||||
|
book = book.subtract(s.getBookValueSold());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return book;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The coupon date the buyer's interest runs from: the last coupon on or before settlement, never before our purchase. */
|
||||||
|
private static LocalDate lastCoupon(final InvestmentPurchase p, final LocalDate settlementDate) {
|
||||||
|
final LocalDate coupon = InvestmentInterestCalculator.lastCouponOnOrBefore(p.getMaturityDate(), settlementDate);
|
||||||
|
return p.getLastIntDate() != null && p.getLastIntDate().isAfter(coupon) ? p.getLastIntDate() : coupon;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Share of the interest receivable still on the books for this coupon period (provisions posted after the last
|
||||||
|
* coupon, less what earlier sales cleared) that belongs to the units sold.
|
||||||
|
*/
|
||||||
|
private BigDecimal receivableShare(final InvestmentPurchase p, final LocalDate lastCoupon, final LocalDate settlementDate,
|
||||||
|
final BigDecimal units) {
|
||||||
|
BigDecimal outstanding = BigDecimal.ZERO;
|
||||||
|
for (final InvestmentInterestPosting x : this.postingRepository.findByPurchaseIdOrderByIntDateAsc(p.getId())) {
|
||||||
|
if (InvestmentInterestCalculator.TYPE_PROVISION.equals(x.getLineType()) && x.getIntDate().isAfter(lastCoupon)
|
||||||
|
&& !x.getIntDate().isAfter(settlementDate)) {
|
||||||
|
outstanding = outstanding.add(x.getAmount());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (final InvestmentSale s : this.saleRepository.findByPurchaseIdOrderByIdAsc(p.getId())) {
|
||||||
|
if (STATUS_SETTLED.equals(s.getStatus()) && s.getSettlementDate().isAfter(lastCoupon) && s.getReceivableCleared() != null) {
|
||||||
|
outstanding = outstanding.subtract(s.getReceivableCleared());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (outstanding.signum() <= 0) {
|
||||||
|
return BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
return outstanding.multiply(units).divide(p.getBalanceUnits(), 2, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void requireActiveDebtHolding(final InvestmentPurchase p) {
|
||||||
|
if (!STATUS_ACTIVE.equals(p.getStatus())) {
|
||||||
|
throw rule("not.active", "Deal " + p.getDealNo() + " is not an active (settled) holding");
|
||||||
|
}
|
||||||
|
if (!InvestmentApiConstants.DEBT_BOND_TYPES.contains(p.getBondType())) {
|
||||||
|
throw rule("type.unsupported", "Sale and redemption are available for Govt Security, Non-SLR and Bond holdings");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The bank the money comes into must be an enabled ASSET detail GL. */
|
||||||
|
private GLAccount postableAssetGl(final Long glAccountId) {
|
||||||
|
final GLAccount gl = this.glAccountRepository.findOneWithNotFoundDetection(glAccountId);
|
||||||
|
if (gl.isHeaderAccount() || gl.isDisabled() || !GLAccountType.ASSET.getValue().equals(gl.getType())) {
|
||||||
|
throw rule("bank.glaccount.invalid", "Bank GL " + gl.getGlCode() + " must be an enabled Asset detail account");
|
||||||
|
}
|
||||||
|
return gl;
|
||||||
|
}
|
||||||
|
|
||||||
|
static GeneralPlatformDomainRuleException rule(final String code, final String message) {
|
||||||
|
return new GeneralPlatformDomainRuleException("error.msg.investmentsale." + code, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.portfolio.investment.data.InvestmentSaleData;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchase;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchaseRepository;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentSaleRepository;
|
||||||
|
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 InvestmentSaleReadPlatformService {
|
||||||
|
|
||||||
|
private final InvestmentSaleRepository saleRepository;
|
||||||
|
private final InvestmentPurchaseRepository purchaseRepository;
|
||||||
|
private final InvestmentSaleQuoter quoter;
|
||||||
|
|
||||||
|
/** Sales and redemptions, newest first — pending ones included. */
|
||||||
|
public List<InvestmentSaleData> retrieveAll() {
|
||||||
|
return this.saleRepository.findAll(Sort.by(Sort.Direction.DESC, "id")).stream().map(InvestmentSaleData::from).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** What a sale would come to, before it is saved. */
|
||||||
|
public InvestmentSaleData quoteSale(final Long purchaseId, final LocalDate dealDate, final String settlementType, final BigDecimal units,
|
||||||
|
final BigDecimal saleRate, final BigDecimal brokerage, final Long bankGlAccountId) {
|
||||||
|
final InvestmentPurchase p = purchase(purchaseId);
|
||||||
|
return InvestmentSaleData.from(this.quoter.quoteSale(p, new InvestmentSaleQuoter.SellRequest(null, dealDate, null, settlementType,
|
||||||
|
units, saleRate, brokerage, bankGlAccountId, null, null), null, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** What the redemption at maturity will post, and the units still available to sell. */
|
||||||
|
public InvestmentSaleData quoteRedemption(final Long purchaseId) {
|
||||||
|
return InvestmentSaleData.from(this.quoter.quoteRedemption(purchase(purchaseId), null, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal availableUnits(final Long purchaseId) {
|
||||||
|
return this.quoter.availableUnits(purchase(purchaseId), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private InvestmentPurchase purchase(final Long purchaseId) {
|
||||||
|
return this.purchaseRepository.findById(purchaseId).orElseThrow(() -> new InvestmentPurchaseNotFoundException(purchaseId));
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
@@ -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 InvestmentSaleWritePlatformService {
|
||||||
|
|
||||||
|
CommandProcessingResult createSale(JsonCommand command);
|
||||||
|
|
||||||
|
CommandProcessingResult settleSale(Long saleId, JsonCommand command);
|
||||||
|
|
||||||
|
CommandProcessingResult deleteSale(Long saleId);
|
||||||
|
|
||||||
|
CommandProcessingResult redeem(Long purchaseId, JsonCommand command);
|
||||||
|
|
||||||
|
CommandProcessingResult writeOff(Long purchaseId, JsonCommand command);
|
||||||
|
}
|
||||||
+282
@@ -0,0 +1,282 @@
|
|||||||
|
/**
|
||||||
|
* 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.STATUS_ACTIVE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.STATUS_REDEEMED;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.STATUS_SOLD;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.AMOUNT;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.BANK_GL_ACCOUNT_ID;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.BROKERAGE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.DEAL_DATE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.DEAL_TIME;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.LOSS_ON_SALE_GL_CODE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.PARTY_ID;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.PREMIUM_AMORTISATION_GL_CODE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.PROFIT_ON_SALE_GL_CODE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.PURCHASE_ID;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.REMARKS;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.SALE_RATE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.SANCTION_AUTHORITY;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.SETTLEMENT_TYPE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.STATUS_PENDING_SETTLEMENT;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.STATUS_SETTLED;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.TYPE_WRITEOFF;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.UNITS;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
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.service.DateUtils;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentParty;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentPartyRepository;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchase;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchaseRepository;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentSale;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentSaleRepository;
|
||||||
|
import org.apache.fineract.portfolio.investment.exception.InvestmentPartyNotFoundException;
|
||||||
|
import org.apache.fineract.portfolio.investment.exception.InvestmentPurchaseNotFoundException;
|
||||||
|
import org.apache.fineract.portfolio.investment.serialization.InvestmentSaleCommandFromApiJsonDeserializer;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sale and redemption of holdings.
|
||||||
|
* <ul>
|
||||||
|
* <li>A sale is saved pending; on or after its settlement date Settle works the figures out again on the books as they
|
||||||
|
* stand and posts, dated the settlement date: Dr Bank (price + interest − brokerage), Dr Brokerage / Cr Investment
|
||||||
|
* (cost), Cr Interest Receivable, Cr Interest Income, Cr Profit or Dr Loss.</li>
|
||||||
|
* <li>A redemption posts at once, dated the maturity date: Dr Bank (face value) / Cr Investment (book value), with a
|
||||||
|
* discount left going to profit and a premium left written off to the amortisation expense.</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class InvestmentSaleWritePlatformServiceImpl implements InvestmentSaleWritePlatformService {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
||||||
|
private static final String BROKERAGE_GL_CODE = "500602";
|
||||||
|
|
||||||
|
private final InvestmentSaleCommandFromApiJsonDeserializer validator;
|
||||||
|
private final InvestmentSaleQuoter quoter;
|
||||||
|
private final InvestmentSaleRepository saleRepository;
|
||||||
|
private final InvestmentPurchaseRepository purchaseRepository;
|
||||||
|
private final InvestmentPartyRepository partyRepository;
|
||||||
|
private final InvestmentJournalPoster journalPoster;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public CommandProcessingResult createSale(final JsonCommand command) {
|
||||||
|
this.validator.validateForSell(command.json());
|
||||||
|
final InvestmentPurchase p = purchase(command.longValueOfParameterNamed(PURCHASE_ID));
|
||||||
|
final InvestmentSale sale = this.saleRepository.saveAndFlush(this.quoter.quoteSale(p, sellRequest(command), null, false));
|
||||||
|
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(sale.getId())
|
||||||
|
.withSubEntityId(p.getId()).withResourceIdAsString(p.getDealNo()).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public CommandProcessingResult settleSale(final Long saleId, final JsonCommand command) {
|
||||||
|
final InvestmentSale pending = sale(saleId);
|
||||||
|
if (!STATUS_PENDING_SETTLEMENT.equals(pending.getStatus())) {
|
||||||
|
throw InvestmentSaleQuoter.rule("settle.not.pending", "This sale is not pending settlement");
|
||||||
|
}
|
||||||
|
final InvestmentPurchase p = pending.getPurchase();
|
||||||
|
if (pending.getSettlementDate().isAfter(DateUtils.getBusinessLocalDate())) {
|
||||||
|
throw InvestmentSaleQuoter.rule("settle.before.date",
|
||||||
|
"The sale can be settled on or after its settlement date " + pending.getSettlementDate().format(DATE));
|
||||||
|
}
|
||||||
|
this.quoter.requireInterestPostedUpTo(p, pending.getSettlementDate());
|
||||||
|
|
||||||
|
// The books may have moved since the sale was saved (a provision posted, another sale settled): work it out again.
|
||||||
|
final InvestmentSale s = this.quoter.quoteSale(p, new InvestmentSaleQuoter.SellRequest(pending.getParty(), pending.getDealDate(),
|
||||||
|
pending.getDealTime(), pending.getSettlementType(), pending.getUnits(), pending.getSaleRate(), pending.getBrokerage(),
|
||||||
|
pending.getBankGlAccount().getId(), pending.getSanctionAuthority(), pending.getRemarks()), saleId, false);
|
||||||
|
// Principal only; the interest from the buyer is a manual entry (shown as a hint on the screen).
|
||||||
|
final InvestmentJournalPoster.Lines lines = new InvestmentJournalPoster.Lines().debit(s.getBankGlAccount().getId(), s.getNetAmount())
|
||||||
|
.debit(this.journalPoster.glId(BROKERAGE_GL_CODE), s.getBrokerage())
|
||||||
|
.credit(p.getInvestmentGlAccount().getId(), s.getBookValueSold().subtract(s.getPremiumCleared()))
|
||||||
|
.credit(InvestmentSaleQuoter.premiumGlId(p), s.getPremiumCleared());
|
||||||
|
profitOrLoss(lines, s.getProfitLoss(), LOSS_ON_SALE_GL_CODE);
|
||||||
|
final CommandProcessingResult posted = this.journalPoster.post(s.getSettlementDate(), p.getDealNo(),
|
||||||
|
"Investment sale settled: deal " + p.getDealNo() + ", " + s.getUnits().stripTrailingZeros().toPlainString() + " units of "
|
||||||
|
+ p.getScript().getScriptCode() + " " + p.getScript().getScriptDesc() + " to " + s.getParty().getPartyName(),
|
||||||
|
lines);
|
||||||
|
|
||||||
|
copyFigures(s, pending);
|
||||||
|
pending.setStatus(STATUS_SETTLED);
|
||||||
|
pending.setGlTransactionId(posted.getTransactionId());
|
||||||
|
pending.setVoucherNo(voucherNo(posted));
|
||||||
|
pending.setSettledOnDate(DateUtils.getBusinessLocalDate());
|
||||||
|
this.saleRepository.saveAndFlush(pending);
|
||||||
|
|
||||||
|
p.setBalanceUnits(p.getBalanceUnits().subtract(pending.getUnits()));
|
||||||
|
if (p.getBalanceUnits().signum() == 0) {
|
||||||
|
p.setStatus(STATUS_SOLD);
|
||||||
|
}
|
||||||
|
this.purchaseRepository.saveAndFlush(p);
|
||||||
|
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(saleId).withSubEntityId(p.getId())
|
||||||
|
.withResourceIdAsString(pending.getVoucherNo()).withTransactionId(posted.getTransactionId()).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public CommandProcessingResult deleteSale(final Long saleId) {
|
||||||
|
final InvestmentSale s = sale(saleId);
|
||||||
|
if (!STATUS_PENDING_SETTLEMENT.equals(s.getStatus())) {
|
||||||
|
throw InvestmentSaleQuoter.rule("sale.settled", "This sale is already settled and posted to GL, so it cannot be dropped");
|
||||||
|
}
|
||||||
|
final Long purchaseId = s.getPurchase().getId();
|
||||||
|
this.saleRepository.delete(s);
|
||||||
|
this.saleRepository.flush();
|
||||||
|
return new CommandProcessingResultBuilder().withEntityId(saleId).withSubEntityId(purchaseId).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public CommandProcessingResult redeem(final Long purchaseId, final JsonCommand command) {
|
||||||
|
this.validator.validateForRedeem(command.json());
|
||||||
|
final InvestmentPurchase p = purchase(purchaseId);
|
||||||
|
final Long bankGlId = command.parameterExists(BANK_GL_ACCOUNT_ID) ? command.longValueOfParameterNamed(BANK_GL_ACCOUNT_ID) : null;
|
||||||
|
final InvestmentSale s = this.quoter.quoteRedemption(p, bankGlId, command.stringValueOfParameterNamed(REMARKS));
|
||||||
|
this.quoter.requireInterestPostedUpTo(p, p.getMaturityDate());
|
||||||
|
|
||||||
|
final InvestmentJournalPoster.Lines lines = new InvestmentJournalPoster.Lines().debit(s.getBankGlAccount().getId(), s.getNetAmount())
|
||||||
|
.credit(p.getInvestmentGlAccount().getId(), s.getBookValueSold().subtract(s.getPremiumCleared()))
|
||||||
|
.credit(InvestmentSaleQuoter.premiumGlId(p), s.getPremiumCleared());
|
||||||
|
profitOrLoss(lines, s.getProfitLoss(), PREMIUM_AMORTISATION_GL_CODE);
|
||||||
|
final CommandProcessingResult posted = this.journalPoster.post(s.getSettlementDate(), p.getDealNo(),
|
||||||
|
"Investment redeemed at maturity: deal " + p.getDealNo() + ", " + p.getScript().getScriptCode() + " "
|
||||||
|
+ p.getScript().getScriptDesc(),
|
||||||
|
lines);
|
||||||
|
|
||||||
|
s.setGlTransactionId(posted.getTransactionId());
|
||||||
|
s.setVoucherNo(voucherNo(posted));
|
||||||
|
s.setSettledOnDate(DateUtils.getBusinessLocalDate());
|
||||||
|
final InvestmentSale saved = this.saleRepository.saveAndFlush(s);
|
||||||
|
p.setBalanceUnits(BigDecimal.ZERO);
|
||||||
|
p.setStatus(STATUS_REDEEMED);
|
||||||
|
this.purchaseRepository.saveAndFlush(p);
|
||||||
|
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(saved.getId())
|
||||||
|
.withSubEntityId(purchaseId).withResourceIdAsString(saved.getVoucherNo()).withTransactionId(posted.getTransactionId())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes part of the book value off as a loss (Jitendra Sir: a security bought for 1 lakh now worth 95,000 is
|
||||||
|
* shown at 95,000): Dr Loss on Investments / Cr Investment, dated today. The units stay held; later sales and the
|
||||||
|
* redemption work from the reduced book value.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public CommandProcessingResult writeOff(final Long purchaseId, final JsonCommand command) {
|
||||||
|
this.validator.validateForWriteOff(command.json());
|
||||||
|
final InvestmentPurchase p = purchase(purchaseId);
|
||||||
|
final BigDecimal amount = command.bigDecimalValueOfParameterNamed(AMOUNT).setScale(2, RoundingMode.HALF_UP);
|
||||||
|
final BigDecimal book = this.quoter.remainingBook(p);
|
||||||
|
if (!STATUS_ACTIVE.equals(p.getStatus())) {
|
||||||
|
throw InvestmentSaleQuoter.rule("not.active", "Deal " + p.getDealNo() + " is not an active (settled) holding");
|
||||||
|
}
|
||||||
|
if (amount.compareTo(book) >= 0) {
|
||||||
|
throw InvestmentSaleQuoter.rule("writeoff.exceeds", "The write-off must be less than the book value of "
|
||||||
|
+ book.stripTrailingZeros().toPlainString() + "; sell or redeem the security to close it");
|
||||||
|
}
|
||||||
|
final LocalDate today = DateUtils.getBusinessLocalDate();
|
||||||
|
final CommandProcessingResult posted = this.journalPoster.post(today, p.getDealNo(),
|
||||||
|
"Investment written off: deal " + p.getDealNo() + ", " + p.getScript().getScriptCode() + " " + p.getScript().getScriptDesc(),
|
||||||
|
new InvestmentJournalPoster.Lines().debit(this.journalPoster.glId(LOSS_ON_SALE_GL_CODE), amount)
|
||||||
|
.credit(p.getInvestmentGlAccount().getId(), amount));
|
||||||
|
|
||||||
|
final InvestmentSale s = new InvestmentSale();
|
||||||
|
s.setPurchase(p);
|
||||||
|
s.setSaleType(TYPE_WRITEOFF);
|
||||||
|
s.setDealDate(today);
|
||||||
|
s.setSettlementDate(today);
|
||||||
|
s.setUnits(BigDecimal.ZERO);
|
||||||
|
s.setFaceValue(BigDecimal.ZERO);
|
||||||
|
s.setConsideration(BigDecimal.ZERO);
|
||||||
|
s.setAccruedDays(0);
|
||||||
|
s.setAccruedInterest(BigDecimal.ZERO);
|
||||||
|
s.setReceivableCleared(BigDecimal.ZERO);
|
||||||
|
s.setBrokerage(BigDecimal.ZERO);
|
||||||
|
s.setBookValueSold(amount);
|
||||||
|
s.setPremiumCleared(BigDecimal.ZERO);
|
||||||
|
s.setProfitLoss(amount.negate());
|
||||||
|
s.setNetAmount(BigDecimal.ZERO);
|
||||||
|
s.setBankGlAccount(p.getCrGlAccount());
|
||||||
|
s.setRemarks(command.stringValueOfParameterNamed(REMARKS).trim());
|
||||||
|
s.setStatus(STATUS_SETTLED);
|
||||||
|
s.setGlTransactionId(posted.getTransactionId());
|
||||||
|
s.setVoucherNo(voucherNo(posted));
|
||||||
|
s.setSettledOnDate(today);
|
||||||
|
final InvestmentSale saved = this.saleRepository.saveAndFlush(s);
|
||||||
|
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(saved.getId())
|
||||||
|
.withSubEntityId(purchaseId).withResourceIdAsString(saved.getVoucherNo()).withTransactionId(posted.getTransactionId())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Profit is credited to income; a shortfall is debited to {@code lossGlCode}. */
|
||||||
|
private void profitOrLoss(final InvestmentJournalPoster.Lines lines, final BigDecimal profitLoss, final String lossGlCode) {
|
||||||
|
if (profitLoss.signum() > 0) {
|
||||||
|
lines.credit(this.journalPoster.glId(PROFIT_ON_SALE_GL_CODE), profitLoss);
|
||||||
|
} else if (profitLoss.signum() < 0) {
|
||||||
|
lines.debit(this.journalPoster.glId(lossGlCode), profitLoss.negate());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private InvestmentSaleQuoter.SellRequest sellRequest(final JsonCommand command) {
|
||||||
|
final Long partyId = command.longValueOfParameterNamed(PARTY_ID);
|
||||||
|
final InvestmentParty party = this.partyRepository.findById(partyId).orElseThrow(() -> new InvestmentPartyNotFoundException(partyId));
|
||||||
|
final LocalDate dealDate = command.localDateValueOfParameterNamed(DEAL_DATE);
|
||||||
|
return new InvestmentSaleQuoter.SellRequest(party, dealDate, command.stringValueOfParameterNamed(DEAL_TIME),
|
||||||
|
command.stringValueOfParameterNamed(SETTLEMENT_TYPE), command.bigDecimalValueOfParameterNamed(UNITS),
|
||||||
|
command.bigDecimalValueOfParameterNamed(SALE_RATE), command.bigDecimalValueOfParameterNamed(BROKERAGE),
|
||||||
|
command.longValueOfParameterNamed(BANK_GL_ACCOUNT_ID), command.stringValueOfParameterNamed(SANCTION_AUTHORITY),
|
||||||
|
command.stringValueOfParameterNamed(REMARKS));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void copyFigures(final InvestmentSale from, final InvestmentSale to) {
|
||||||
|
to.setAccruedDays(from.getAccruedDays());
|
||||||
|
to.setAccruedInterest(from.getAccruedInterest());
|
||||||
|
to.setReceivableCleared(from.getReceivableCleared());
|
||||||
|
to.setBookValueSold(from.getBookValueSold());
|
||||||
|
to.setPremiumCleared(from.getPremiumCleared());
|
||||||
|
to.setProfitLoss(from.getProfitLoss());
|
||||||
|
to.setNetAmount(from.getNetAmount());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String voucherNo(final CommandProcessingResult posted) {
|
||||||
|
final Object voucherNo = posted.getChanges() == null ? null : posted.getChanges().get("voucherNo");
|
||||||
|
return voucherNo == null ? null : voucherNo.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private InvestmentPurchase purchase(final Long purchaseId) {
|
||||||
|
return this.purchaseRepository.findById(purchaseId).orElseThrow(() -> new InvestmentPurchaseNotFoundException(purchaseId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private InvestmentSale sale(final Long saleId) {
|
||||||
|
return this.saleRepository.findById(saleId)
|
||||||
|
.orElseThrow(() -> InvestmentSaleQuoter.rule("sale.not.found", "Investment sale " + saleId + " does not exist"));
|
||||||
|
}
|
||||||
|
}
|
||||||
+171
@@ -0,0 +1,171 @@
|
|||||||
|
/**
|
||||||
|
* 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.STATUS_ACTIVE;
|
||||||
|
|
||||||
|
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.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||||
|
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
|
import org.apache.fineract.portfolio.investment.api.InvestmentApiConstants;
|
||||||
|
import org.apache.fineract.portfolio.investment.data.InvestmentSecurityTransferData;
|
||||||
|
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.domain.InvestmentSecurityTransfer;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentSecurityTransferRepository;
|
||||||
|
import org.apache.fineract.portfolio.investment.exception.InvestmentPurchaseNotFoundException;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GOI Category Transfer as Jitendra Sir describes it: a holding is replaced by another government security of the same
|
||||||
|
* terms (same coupon rate, maturity and unit value, e.g. one state loan for another). Units, book value and the interest
|
||||||
|
* schedule carry over unchanged; when the new script maps to a different investment GL the book value is moved across,
|
||||||
|
* Dr new GL / Cr old GL, dated the transfer date.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class InvestmentSecurityTransferService {
|
||||||
|
|
||||||
|
public static final String SCRIPT_ID = "scriptId";
|
||||||
|
public static final String REMARKS = "remarks";
|
||||||
|
private static final Set<String> PARAMS = Set.of(SCRIPT_ID, REMARKS, "locale", "dateFormat");
|
||||||
|
|
||||||
|
private final FromJsonHelper fromApiJsonHelper;
|
||||||
|
private final InvestmentPurchaseRepository purchaseRepository;
|
||||||
|
private final InvestmentScriptRepository scriptRepository;
|
||||||
|
private final InvestmentSecurityTransferRepository transferRepository;
|
||||||
|
private final InvestmentSaleQuoter quoter;
|
||||||
|
private final InvestmentJournalPoster journalPoster;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<InvestmentSecurityTransferData> retrieveAll() {
|
||||||
|
return this.transferRepository.findAll(Sort.by(Sort.Direction.DESC, "id")).stream().map(InvestmentSecurityTransferData::from)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public CommandProcessingResult transfer(final Long purchaseId, final JsonCommand command) {
|
||||||
|
validate(command.json());
|
||||||
|
final InvestmentPurchase p = this.purchaseRepository.findById(purchaseId)
|
||||||
|
.orElseThrow(() -> new InvestmentPurchaseNotFoundException(purchaseId));
|
||||||
|
final Long scriptId = command.longValueOfParameterNamed(SCRIPT_ID);
|
||||||
|
final InvestmentScript to = this.scriptRepository.findById(scriptId).orElseThrow(() -> new InvestmentScriptNotFoundException(scriptId));
|
||||||
|
final InvestmentScript from = p.getScript();
|
||||||
|
if (!STATUS_ACTIVE.equals(p.getStatus()) || !InvestmentApiConstants.DEBT_BOND_TYPES.contains(p.getBondType())) {
|
||||||
|
throw rule("not.active", "Deal " + p.getDealNo() + " is not an active Govt Security / Bond holding");
|
||||||
|
}
|
||||||
|
if (this.quoter.availableUnits(p, null).compareTo(p.getBalanceUnits()) != 0) {
|
||||||
|
throw rule("pending.sale", "Deal " + p.getDealNo() + " has a pending sale; settle or drop it first");
|
||||||
|
}
|
||||||
|
if (to.getId().equals(from.getId())) {
|
||||||
|
throw rule("same.script", "Choose a different security to transfer to");
|
||||||
|
}
|
||||||
|
if (!to.isActive()) {
|
||||||
|
throw rule("script.inactive", "Script " + to.getScriptCode() + " is inactive");
|
||||||
|
}
|
||||||
|
if (!to.getBondType().equals(p.getBondType()) || !sameTerms(from, to)) {
|
||||||
|
throw rule("terms.differ", "Script " + to.getScriptCode() + " must have the same bond type, coupon rate, maturity date and "
|
||||||
|
+ "unit value as " + from.getScriptCode() + "; only the security changes, everything else stays the same");
|
||||||
|
}
|
||||||
|
|
||||||
|
final LocalDate today = DateUtils.getBusinessLocalDate();
|
||||||
|
final BigDecimal book = this.quoter.remainingBook(p);
|
||||||
|
final InvestmentSecurityTransfer t = new InvestmentSecurityTransfer();
|
||||||
|
t.setPurchase(p);
|
||||||
|
t.setFromScript(from);
|
||||||
|
t.setToScript(to);
|
||||||
|
t.setFromGlAccount(p.getInvestmentGlAccount());
|
||||||
|
t.setToGlAccount(to.getGlAccount());
|
||||||
|
t.setTransferDate(today);
|
||||||
|
t.setUnits(p.getBalanceUnits());
|
||||||
|
t.setBookValue(book);
|
||||||
|
t.setRemarks(StringUtils.trimToNull(command.stringValueOfParameterNamed(REMARKS)));
|
||||||
|
String transactionId = null;
|
||||||
|
if (!to.getGlAccount().getId().equals(p.getInvestmentGlAccount().getId())) {
|
||||||
|
// Unamortised premium kept in its own GL (100707) stays there; only the investment GL's part moves.
|
||||||
|
final BigDecimal moved = p.getPremiumGlAccount() != null ? book.subtract(this.quoter.unamortisedPremium(p)) : book;
|
||||||
|
final CommandProcessingResult posted = this.journalPoster.post(today, p.getDealNo(),
|
||||||
|
"GOI category transfer: deal " + p.getDealNo() + " from " + from.getScriptCode() + " " + from.getScriptDesc() + " to "
|
||||||
|
+ to.getScriptCode() + " " + to.getScriptDesc(),
|
||||||
|
new InvestmentJournalPoster.Lines().debit(to.getGlAccount().getId(), moved).credit(p.getInvestmentGlAccount().getId(),
|
||||||
|
moved));
|
||||||
|
transactionId = posted.getTransactionId();
|
||||||
|
final Object voucherNo = posted.getChanges() == null ? null : posted.getChanges().get("voucherNo");
|
||||||
|
t.setGlTransactionId(transactionId);
|
||||||
|
t.setVoucherNo(voucherNo == null ? null : voucherNo.toString());
|
||||||
|
}
|
||||||
|
final InvestmentSecurityTransfer saved = this.transferRepository.saveAndFlush(t);
|
||||||
|
p.setScript(to);
|
||||||
|
p.setInvestmentGlAccount(to.getGlAccount());
|
||||||
|
this.purchaseRepository.saveAndFlush(p);
|
||||||
|
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(saved.getId()).withSubEntityId(purchaseId)
|
||||||
|
.withResourceIdAsString(saved.getVoucherNo() == null ? p.getDealNo() : saved.getVoucherNo()).withTransactionId(transactionId)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean sameTerms(final InvestmentScript a, final InvestmentScript b) {
|
||||||
|
return a.getIntRate() != null && b.getIntRate() != null && a.getIntRate().compareTo(b.getIntRate()) == 0
|
||||||
|
&& a.getMaturityDate() != null && a.getMaturityDate().equals(b.getMaturityDate())
|
||||||
|
&& a.getFaceValuePerUnit().compareTo(b.getFaceValuePerUnit()) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validate(final String json) {
|
||||||
|
if (StringUtils.isBlank(json)) {
|
||||||
|
throw new InvalidJsonException();
|
||||||
|
}
|
||||||
|
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
||||||
|
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, PARAMS);
|
||||||
|
final JsonElement element = this.fromApiJsonHelper.parse(json);
|
||||||
|
final List<ApiParameterError> errors = new ArrayList<>();
|
||||||
|
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource("investmenttransfer");
|
||||||
|
v.reset().parameter(SCRIPT_ID).value(this.fromApiJsonHelper.extractLongNamed(SCRIPT_ID, element)).notNull().longGreaterThanZero();
|
||||||
|
v.reset().parameter(REMARKS).value(this.fromApiJsonHelper.extractStringNamed(REMARKS, element)).ignoreIfNull()
|
||||||
|
.notExceedingLengthOf(500);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GeneralPlatformDomainRuleException rule(final String code, final String message) {
|
||||||
|
return new GeneralPlatformDomainRuleException("error.msg.investmenttransfer." + code, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -180,4 +180,7 @@
|
|||||||
<include file="parts/0596_investment_daily_rate.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0596_investment_daily_rate.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0597_investment_purchase_settlement.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0597_investment_purchase_settlement.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0600_investment_interest_posting.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0600_investment_interest_posting.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0601_investment_sale_redemption.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0602_investment_security_transfer.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0603_investment_premium_amortisation.xml" relativeToChangelogFile="true"/>
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|||||||
+108
@@ -0,0 +1,108 @@
|
|||||||
|
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!--
|
||||||
|
|
||||||
|
Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
or more contributor license agreements. See the NOTICE file
|
||||||
|
distributed with this work for additional information
|
||||||
|
regarding copyright ownership. The ASF licenses this file
|
||||||
|
to you under the Apache License, Version 2.0 (the
|
||||||
|
"License"); you may not use this file except in compliance
|
||||||
|
with the License. You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing,
|
||||||
|
software distributed under the License is distributed on an
|
||||||
|
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
KIND, either express or implied. See the License for the
|
||||||
|
specific language governing permissions and limitations
|
||||||
|
under the License.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.9.xsd">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Sale (before maturity, all or part of the units) and redemption (at maturity) of investment holdings.
|
||||||
|
A sale is saved pending and posted to GL on its settlement date (settlement-date accounting, like the purchase);
|
||||||
|
a redemption is posted at once, dated the maturity date.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<changeSet author="cbs-anshul" id="0601-1-create-investment-sale">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><tableExists tableName="m_investment_sale"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<createTable tableName="m_investment_sale">
|
||||||
|
<column name="id" type="BIGINT" autoIncrement="true"><constraints nullable="false" primaryKey="true"/></column>
|
||||||
|
<column name="purchase_id" type="BIGINT"><constraints nullable="false"/></column>
|
||||||
|
<column name="sale_type" type="VARCHAR(10)"><constraints nullable="false"/></column>
|
||||||
|
<column name="party_id" type="BIGINT"/>
|
||||||
|
<column name="deal_date" type="DATE"><constraints nullable="false"/></column>
|
||||||
|
<column name="deal_time" type="VARCHAR(8)"/>
|
||||||
|
<column name="settlement_type" type="VARCHAR(5)"/>
|
||||||
|
<column name="settlement_date" type="DATE"><constraints nullable="false"/></column>
|
||||||
|
<column name="units" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
|
||||||
|
<column name="face_value" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
|
||||||
|
<column name="sale_rate" type="DECIMAL(19,6)"/>
|
||||||
|
<column name="consideration" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
|
||||||
|
<column name="accrued_days" type="INT"/>
|
||||||
|
<column name="accrued_interest" type="DECIMAL(19,6)"/>
|
||||||
|
<column name="receivable_cleared" type="DECIMAL(19,6)"/>
|
||||||
|
<column name="brokerage" type="DECIMAL(19,6)"/>
|
||||||
|
<column name="book_value_sold" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
|
||||||
|
<column name="profit_loss" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
|
||||||
|
<column name="net_amount" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
|
||||||
|
<column name="bank_gl_account_id" type="BIGINT"><constraints nullable="false"/></column>
|
||||||
|
<column name="sanction_authority" type="VARCHAR(100)"/>
|
||||||
|
<column name="remarks" type="VARCHAR(500)"/>
|
||||||
|
<column name="status" type="VARCHAR(20)"><constraints nullable="false"/></column>
|
||||||
|
<column name="gl_transaction_id" type="VARCHAR(50)"/>
|
||||||
|
<column name="voucher_no" type="VARCHAR(30)"/>
|
||||||
|
<column name="settled_on_date" type="DATE"/>
|
||||||
|
<column name="created_by" type="BIGINT"><constraints nullable="false"/></column>
|
||||||
|
<column name="created_on_utc" type="DATETIME"><constraints nullable="false"/></column>
|
||||||
|
<column name="last_modified_by" type="BIGINT"><constraints nullable="false"/></column>
|
||||||
|
<column name="last_modified_on_utc" type="DATETIME"><constraints nullable="false"/></column>
|
||||||
|
</createTable>
|
||||||
|
<addForeignKeyConstraint baseTableName="m_investment_sale" baseColumnNames="purchase_id" constraintName="fk_investment_sale_purchase"
|
||||||
|
referencedTableName="m_investment_purchase" referencedColumnNames="id"/>
|
||||||
|
<addForeignKeyConstraint baseTableName="m_investment_sale" baseColumnNames="party_id" constraintName="fk_investment_sale_party"
|
||||||
|
referencedTableName="m_investment_party" referencedColumnNames="id"/>
|
||||||
|
<addForeignKeyConstraint baseTableName="m_investment_sale" baseColumnNames="bank_gl_account_id" constraintName="fk_investment_sale_bank_gl"
|
||||||
|
referencedTableName="acc_gl_account" referencedColumnNames="id"/>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<!-- Sell follows the purchase: saving and settling go through maker-checker; a redemption posts at once like an interest voucher. -->
|
||||||
|
<changeSet author="cbs-anshul" id="0601-2-investment-sale-permissions">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE entity_name = 'INVESTMENTSALE'</sqlCheck>
|
||||||
|
</preConditions>
|
||||||
|
<sql>
|
||||||
|
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) VALUES
|
||||||
|
('portfolio', 'READ_INVESTMENTSALE', 'INVESTMENTSALE', 'READ', FALSE),
|
||||||
|
('portfolio', 'CREATE_INVESTMENTSALE', 'INVESTMENTSALE', 'CREATE', TRUE),
|
||||||
|
('portfolio', 'CREATE_INVESTMENTSALE_CHECKER', 'INVESTMENTSALE', 'CREATE_CHECKER', FALSE),
|
||||||
|
('portfolio', 'SETTLE_INVESTMENTSALE', 'INVESTMENTSALE', 'SETTLE', TRUE),
|
||||||
|
('portfolio', 'SETTLE_INVESTMENTSALE_CHECKER', 'INVESTMENTSALE', 'SETTLE_CHECKER', FALSE),
|
||||||
|
('portfolio', 'DELETE_INVESTMENTSALE', 'INVESTMENTSALE', 'DELETE', FALSE),
|
||||||
|
('portfolio', 'DELETE_INVESTMENTSALE_CHECKER', 'INVESTMENTSALE', 'DELETE_CHECKER', FALSE),
|
||||||
|
('portfolio', 'REDEEM_INVESTMENTSALE', 'INVESTMENTSALE', 'REDEEM', FALSE),
|
||||||
|
('portfolio', 'REDEEM_INVESTMENTSALE_CHECKER', 'INVESTMENTSALE', 'REDEEM_CHECKER', FALSE)
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<!-- Same roles as the other investment screens. -->
|
||||||
|
<changeSet author="cbs-anshul" id="0601-3-grant-investment-sale-permissions">
|
||||||
|
<sql>
|
||||||
|
INSERT INTO m_role_permission (role_id, permission_id)
|
||||||
|
SELECT rp.role_id, p.id
|
||||||
|
FROM m_role_permission rp
|
||||||
|
JOIN m_permission menu ON menu.id = rp.permission_id AND menu.code = 'MENU_INVESTMENT'
|
||||||
|
CROSS JOIN m_permission p
|
||||||
|
WHERE p.code IN ('READ_INVESTMENTSALE', 'CREATE_INVESTMENTSALE', 'SETTLE_INVESTMENTSALE', 'DELETE_INVESTMENTSALE',
|
||||||
|
'REDEEM_INVESTMENTSALE')
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM m_role_permission x WHERE x.role_id = rp.role_id AND x.permission_id = p.id)
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
</databaseChangeLog>
|
||||||
+113
@@ -0,0 +1,113 @@
|
|||||||
|
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!--
|
||||||
|
|
||||||
|
Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
or more contributor license agreements. See the NOTICE file
|
||||||
|
distributed with this work for additional information
|
||||||
|
regarding copyright ownership. The ASF licenses this file
|
||||||
|
to you under the Apache License, Version 2.0 (the
|
||||||
|
"License"); you may not use this file except in compliance
|
||||||
|
with the License. You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing,
|
||||||
|
software distributed under the License is distributed on an
|
||||||
|
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
KIND, either express or implied. See the License for the
|
||||||
|
specific language governing permissions and limitations
|
||||||
|
under the License.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.9.xsd">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
GOI Category Transfer (Jitendra Sir, 15-10-2026): a holding moves from one government security script to another
|
||||||
|
of the same terms (e.g. one state's loan to another's) — "replace to new". Units, book value and the interest
|
||||||
|
schedule stay; the investment GL moves only if the new script maps to a different GL.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<changeSet author="cbs-anshul" id="0602-1-create-investment-security-transfer">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><tableExists tableName="m_investment_security_transfer"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<createTable tableName="m_investment_security_transfer">
|
||||||
|
<column name="id" type="BIGINT" autoIncrement="true"><constraints nullable="false" primaryKey="true"/></column>
|
||||||
|
<column name="purchase_id" type="BIGINT"><constraints nullable="false"/></column>
|
||||||
|
<column name="from_script_id" type="BIGINT"><constraints nullable="false"/></column>
|
||||||
|
<column name="to_script_id" type="BIGINT"><constraints nullable="false"/></column>
|
||||||
|
<column name="from_gl_account_id" type="BIGINT"><constraints nullable="false"/></column>
|
||||||
|
<column name="to_gl_account_id" type="BIGINT"><constraints nullable="false"/></column>
|
||||||
|
<column name="transfer_date" type="DATE"><constraints nullable="false"/></column>
|
||||||
|
<column name="units" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
|
||||||
|
<column name="book_value" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
|
||||||
|
<column name="remarks" type="VARCHAR(500)"/>
|
||||||
|
<column name="gl_transaction_id" type="VARCHAR(50)"/>
|
||||||
|
<column name="voucher_no" type="VARCHAR(30)"/>
|
||||||
|
<column name="created_by" type="BIGINT"><constraints nullable="false"/></column>
|
||||||
|
<column name="created_on_utc" type="DATETIME"><constraints nullable="false"/></column>
|
||||||
|
<column name="last_modified_by" type="BIGINT"><constraints nullable="false"/></column>
|
||||||
|
<column name="last_modified_on_utc" type="DATETIME"><constraints nullable="false"/></column>
|
||||||
|
</createTable>
|
||||||
|
<addForeignKeyConstraint baseTableName="m_investment_security_transfer" baseColumnNames="purchase_id" constraintName="fk_inv_sec_transfer_purchase"
|
||||||
|
referencedTableName="m_investment_purchase" referencedColumnNames="id"/>
|
||||||
|
<addForeignKeyConstraint baseTableName="m_investment_security_transfer" baseColumnNames="from_script_id" constraintName="fk_inv_sec_transfer_from_script"
|
||||||
|
referencedTableName="m_investment_script" referencedColumnNames="id"/>
|
||||||
|
<addForeignKeyConstraint baseTableName="m_investment_security_transfer" baseColumnNames="to_script_id" constraintName="fk_inv_sec_transfer_to_script"
|
||||||
|
referencedTableName="m_investment_script" referencedColumnNames="id"/>
|
||||||
|
<addForeignKeyConstraint baseTableName="m_investment_security_transfer" baseColumnNames="from_gl_account_id" constraintName="fk_inv_sec_transfer_from_gl"
|
||||||
|
referencedTableName="acc_gl_account" referencedColumnNames="id"/>
|
||||||
|
<addForeignKeyConstraint baseTableName="m_investment_security_transfer" baseColumnNames="to_gl_account_id" constraintName="fk_inv_sec_transfer_to_gl"
|
||||||
|
referencedTableName="acc_gl_account" referencedColumnNames="id"/>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet author="cbs-anshul" id="0602-2-investment-security-transfer-permissions">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE entity_name = 'INVESTMENTTRANSFER'</sqlCheck>
|
||||||
|
</preConditions>
|
||||||
|
<sql>
|
||||||
|
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) VALUES
|
||||||
|
('portfolio', 'READ_INVESTMENTTRANSFER', 'INVESTMENTTRANSFER', 'READ', FALSE),
|
||||||
|
('portfolio', 'CREATE_INVESTMENTTRANSFER', 'INVESTMENTTRANSFER', 'CREATE', FALSE),
|
||||||
|
('portfolio', 'CREATE_INVESTMENTTRANSFER_CHECKER', 'INVESTMENTTRANSFER', 'CREATE_CHECKER', FALSE)
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<!-- Same roles as the other investment screens. -->
|
||||||
|
<changeSet author="cbs-anshul" id="0602-3-grant-investment-security-transfer-permissions">
|
||||||
|
<sql>
|
||||||
|
INSERT INTO m_role_permission (role_id, permission_id)
|
||||||
|
SELECT rp.role_id, p.id
|
||||||
|
FROM m_role_permission rp
|
||||||
|
JOIN m_permission menu ON menu.id = rp.permission_id AND menu.code = 'MENU_INVESTMENT'
|
||||||
|
CROSS JOIN m_permission p
|
||||||
|
WHERE p.code IN ('READ_INVESTMENTTRANSFER', 'CREATE_INVESTMENTTRANSFER')
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM m_role_permission x WHERE x.role_id = rp.role_id AND x.permission_id = p.id)
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
<!-- Write-off of part of a holding's book value (Jitendra Sir, 15-10-2026): posts at once, like a redemption. -->
|
||||||
|
<changeSet author="cbs-anshul" id="0602-4-investment-writeoff-permissions">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'WRITEOFF_INVESTMENTSALE'</sqlCheck>
|
||||||
|
</preConditions>
|
||||||
|
<sql>
|
||||||
|
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) VALUES
|
||||||
|
('portfolio', 'WRITEOFF_INVESTMENTSALE', 'INVESTMENTSALE', 'WRITEOFF', FALSE),
|
||||||
|
('portfolio', 'WRITEOFF_INVESTMENTSALE_CHECKER', 'INVESTMENTSALE', 'WRITEOFF_CHECKER', FALSE)
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet author="cbs-anshul" id="0602-5-grant-investment-writeoff-permission">
|
||||||
|
<sql>
|
||||||
|
INSERT INTO m_role_permission (role_id, permission_id)
|
||||||
|
SELECT rp.role_id, p.id
|
||||||
|
FROM m_role_permission rp
|
||||||
|
JOIN m_permission menu ON menu.id = rp.permission_id AND menu.code = 'MENU_INVESTMENT'
|
||||||
|
CROSS JOIN m_permission p
|
||||||
|
WHERE p.code = 'WRITEOFF_INVESTMENTSALE'
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM m_role_permission x WHERE x.role_id = rp.role_id AND x.permission_id = p.id)
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
</databaseChangeLog>
|
||||||
+83
@@ -0,0 +1,83 @@
|
|||||||
|
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!--
|
||||||
|
|
||||||
|
Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
or more contributor license agreements. See the NOTICE file
|
||||||
|
distributed with this work for additional information
|
||||||
|
regarding copyright ownership. The ASF licenses this file
|
||||||
|
to you under the Apache License, Version 2.0 (the
|
||||||
|
"License"); you may not use this file except in compliance
|
||||||
|
with the License. You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing,
|
||||||
|
software distributed under the License is distributed on an
|
||||||
|
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
KIND, either express or implied. See the License for the
|
||||||
|
specific language governing permissions and limitations
|
||||||
|
under the License.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.9.xsd">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Premium amortisation of HTM holdings (RBI: premium over face value is amortised over the period remaining to
|
||||||
|
maturity; Jitendra Sir: kept in its own GL 100707). New settlements book the premium to that GL; each sale,
|
||||||
|
redemption and amortisation records how much premium it took off.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<changeSet author="cbs-anshul" id="0603-1-investment-premium-columns">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><columnExists tableName="m_investment_purchase" columnName="premium_gl_account_id"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<addColumn tableName="m_investment_purchase">
|
||||||
|
<column name="premium_gl_account_id" type="BIGINT"/>
|
||||||
|
</addColumn>
|
||||||
|
<addForeignKeyConstraint baseTableName="m_investment_purchase" baseColumnNames="premium_gl_account_id"
|
||||||
|
constraintName="fk_investment_purchase_premium_gl" referencedTableName="acc_gl_account" referencedColumnNames="id"/>
|
||||||
|
<addColumn tableName="m_investment_sale">
|
||||||
|
<column name="premium_cleared" type="DECIMAL(19,6)"/>
|
||||||
|
</addColumn>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Sales and redemptions settled before premium tracking took premium off in proportion to units (nothing had been
|
||||||
|
amortised yet), so their share is premium x units sold / units bought; write-offs took none.
|
||||||
|
-->
|
||||||
|
<changeSet author="cbs-anshul" id="0603-4-backfill-investment-premium-cleared">
|
||||||
|
<sql>
|
||||||
|
UPDATE m_investment_sale SET premium_cleared = COALESCE((
|
||||||
|
SELECT ROUND(p.premium_discount * m_investment_sale.units / p.units, 2)
|
||||||
|
FROM m_investment_purchase p
|
||||||
|
WHERE p.id = m_investment_sale.purchase_id AND p.premium_discount > 0 AND p.units > 0), 0)
|
||||||
|
WHERE premium_cleared IS NULL AND sale_type IN ('SELL', 'REDEEM');
|
||||||
|
UPDATE m_investment_sale SET premium_cleared = 0 WHERE premium_cleared IS NULL;
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet author="cbs-anshul" id="0603-2-investment-amortise-permissions">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'AMORTISE_INVESTMENTSALE'</sqlCheck>
|
||||||
|
</preConditions>
|
||||||
|
<sql>
|
||||||
|
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) VALUES
|
||||||
|
('portfolio', 'AMORTISE_INVESTMENTSALE', 'INVESTMENTSALE', 'AMORTISE', FALSE),
|
||||||
|
('portfolio', 'AMORTISE_INVESTMENTSALE_CHECKER', 'INVESTMENTSALE', 'AMORTISE_CHECKER', FALSE)
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet author="cbs-anshul" id="0603-3-grant-investment-amortise-permission">
|
||||||
|
<sql>
|
||||||
|
INSERT INTO m_role_permission (role_id, permission_id)
|
||||||
|
SELECT rp.role_id, p.id
|
||||||
|
FROM m_role_permission rp
|
||||||
|
JOIN m_permission menu ON menu.id = rp.permission_id AND menu.code = 'MENU_INVESTMENT'
|
||||||
|
CROSS JOIN m_permission p
|
||||||
|
WHERE p.code = 'AMORTISE_INVESTMENTSALE'
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM m_role_permission x WHERE x.role_id = rp.role_id AND x.permission_id = p.id)
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
</databaseChangeLog>
|
||||||
Reference in New Issue
Block a user