feat: add portfolio investment api constants and domain models
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Waiting to run
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Waiting to run
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Waiting to run
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Waiting to run
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Waiting to run
Fineract Documentation build / build (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 4 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 3 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 7 of 10) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-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 Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Waiting to run
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Waiting to run
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Waiting to run
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Waiting to run
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Waiting to run
Fineract Documentation build / build (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 4 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 3 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 7 of 10) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-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:
+9
@@ -86,6 +86,15 @@ public final class InvestmentApiConstants {
|
|||||||
|
|
||||||
public static final String PARTY_CODE_PREFIX = "PTY-";
|
public static final String PARTY_CODE_PREFIX = "PTY-";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deal slip: how the deal was confirmed to the counterparty (entered with the deal), and the counterparty's
|
||||||
|
* confirmation the back office records when it settles the deal.
|
||||||
|
*/
|
||||||
|
public static final Set<String> CONFIRMATION_MODES = Set.of("EMAIL", "NDS_OM", "FAX", "LETTER", "PHONE");
|
||||||
|
public static final String CONFIRMATION_MODE = "confirmationMode";
|
||||||
|
public static final String CONFIRMATION_RECEIVED_ON = "confirmationReceivedOn";
|
||||||
|
public static final String CONFIRMATION_REF = "confirmationRef";
|
||||||
|
|
||||||
// Common parameters
|
// Common parameters
|
||||||
public static final String BOND_TYPE = "bondType";
|
public static final String BOND_TYPE = "bondType";
|
||||||
public static final String GL_ACCOUNT_ID = "glAccountId";
|
public static final String GL_ACCOUNT_ID = "glAccountId";
|
||||||
|
|||||||
+72
@@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.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.GET;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.QueryParam;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.investment.data.InvestmentDealSlipData;
|
||||||
|
import org.apache.fineract.portfolio.investment.service.InvestmentDealSlipService;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Path("/v1/investment/deal-slips")
|
||||||
|
@Component
|
||||||
|
@Tag(name = "Investment Deal Slips", description = "Serially numbered deal slips of every purchase, sale, refund, redemption and "
|
||||||
|
+ "switch, cancelled ones included, with the confirmation and who entered, approved and settled each deal.")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class InvestmentDealSlipApiResource {
|
||||||
|
|
||||||
|
/** Default window when no dates are given: the last 30 days. */
|
||||||
|
private static final int DEFAULT_DAYS = 30;
|
||||||
|
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final InvestmentDealSlipService dealSlipService;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Deal slip register", description = "Deals dated fromDate to toDate (dd-MM-yyyy; default the last 30 days) "
|
||||||
|
+ "in slip number order; or the one slip numbered slipNo (a purchase deal number or a sale number).")
|
||||||
|
public List<InvestmentDealSlipData> retrieve(@QueryParam("fromDate") @Parameter(description = "dd-MM-yyyy") final String fromDate,
|
||||||
|
@QueryParam("toDate") @Parameter(description = "dd-MM-yyyy") final String toDate,
|
||||||
|
@QueryParam("slipNo") @Parameter(description = "202627000001 or 202627S00001") final String slipNo) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(InvestmentPurchaseApiConstants.ENTITY);
|
||||||
|
if (StringUtils.isNotBlank(slipNo)) {
|
||||||
|
return this.dealSlipService.slip(slipNo);
|
||||||
|
}
|
||||||
|
final LocalDate today = DateUtils.getBusinessLocalDate();
|
||||||
|
final LocalDate to = InvestmentApiDates.parse(toDate, today, "To date");
|
||||||
|
final LocalDate from = InvestmentApiDates.parse(fromDate, to.minusDays(DEFAULT_DAYS), "From date");
|
||||||
|
if (to.isBefore(from)) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.investment.dealslip.dates", "The To date cannot be before the From date");
|
||||||
|
}
|
||||||
|
return this.dealSlipService.register(from, to);
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-1
@@ -72,7 +72,10 @@ public final class InvestmentPurchaseApiConstants {
|
|||||||
|
|
||||||
public static final Set<String> PARAMS = Set.of(BOND_TYPE, SCRIPT_ID, PARTY_ID, DEAL_DATE, DEAL_TIME, SETTLEMENT_TYPE, FACE_VALUE,
|
public static final Set<String> PARAMS = Set.of(BOND_TYPE, SCRIPT_ID, PARTY_ID, DEAL_DATE, DEAL_TIME, SETTLEMENT_TYPE, FACE_VALUE,
|
||||||
PURCHASE_RATE, CONSIDER_AMT, NAV, STAMP_DUTY, BROKERAGE, OTHER_CHARGES, CATEGORY, LAST_INT_DATE, CR_GL_ACCOUNT_ID, SUB_TYPE,
|
PURCHASE_RATE, CONSIDER_AMT, NAV, STAMP_DUTY, BROKERAGE, OTHER_CHARGES, CATEGORY, LAST_INT_DATE, CR_GL_ACCOUNT_ID, SUB_TYPE,
|
||||||
OTHER_DETAILS, LOCALE, DATE_FORMAT);
|
OTHER_DETAILS, InvestmentApiConstants.CONFIRMATION_MODE, LOCALE, DATE_FORMAT);
|
||||||
|
/** Settle may carry the counterparty's confirmation of the deal. */
|
||||||
|
public static final Set<String> SETTLE_PARAMS = Set.of(InvestmentApiConstants.CONFIRMATION_RECEIVED_ON,
|
||||||
|
InvestmentApiConstants.CONFIRMATION_REF, LOCALE, DATE_FORMAT);
|
||||||
|
|
||||||
/** Dividend received on a share holding: Dr Bank / Cr 400404 Dividend on Investments, dated the day it is received. */
|
/** Dividend received on a share holding: Dr Bank / Cr 400404 Dividend on Investments, dated the day it is received. */
|
||||||
public static final String DIVIDEND_GL_CODE = "400404";
|
public static final String DIVIDEND_GL_CODE = "400404";
|
||||||
|
|||||||
+11
-1
@@ -39,6 +39,11 @@ public final class InvestmentSaleApiConstants {
|
|||||||
|
|
||||||
public static final String STATUS_PENDING_SETTLEMENT = "PENDING_SETTLEMENT";
|
public static final String STATUS_PENDING_SETTLEMENT = "PENDING_SETTLEMENT";
|
||||||
public static final String STATUS_SETTLED = "SETTLED";
|
public static final String STATUS_SETTLED = "SETTLED";
|
||||||
|
/** Dropped before settlement; kept so its sale number stays accounted for in the deal slip register. */
|
||||||
|
public static final String STATUS_DROPPED = "DROPPED";
|
||||||
|
|
||||||
|
/** Sales that are deals with a deal slip of their own (a sale number); write-offs and amortisation are not. */
|
||||||
|
public static final Set<String> NUMBERED_TYPES = Set.of(TYPE_SELL, TYPE_REDEEM, TYPE_SWITCH);
|
||||||
|
|
||||||
/** Profit on sale / redemption (income) and loss on sale / redemption (expense). */
|
/** 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 PROFIT_ON_SALE_GL_CODE = "400405";
|
||||||
@@ -59,6 +64,7 @@ public final class InvestmentSaleApiConstants {
|
|||||||
public static final String BANK_GL_ACCOUNT_ID = "bankGlAccountId";
|
public static final String BANK_GL_ACCOUNT_ID = "bankGlAccountId";
|
||||||
public static final String SANCTION_AUTHORITY = "sanctionAuthority";
|
public static final String SANCTION_AUTHORITY = "sanctionAuthority";
|
||||||
public static final String REMARKS = "remarks";
|
public static final String REMARKS = "remarks";
|
||||||
|
public static final String BROKER_CODE = "brokerCode";
|
||||||
public static final String AMOUNT = "amount";
|
public static final String AMOUNT = "amount";
|
||||||
public static final String FY_END = "fyEnd";
|
public static final String FY_END = "fyEnd";
|
||||||
public static final String SWITCH_DATE = "switchDate";
|
public static final String SWITCH_DATE = "switchDate";
|
||||||
@@ -70,7 +76,11 @@ public final class InvestmentSaleApiConstants {
|
|||||||
public static final String DATE_FORMAT = "dateFormat";
|
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,
|
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);
|
BROKERAGE, BANK_GL_ACCOUNT_ID, SANCTION_AUTHORITY, REMARKS, BROKER_CODE, InvestmentApiConstants.CONFIRMATION_MODE, LOCALE,
|
||||||
|
DATE_FORMAT);
|
||||||
|
/** Settle may carry the counterparty's confirmation of the deal. */
|
||||||
|
public static final Set<String> SETTLE_PARAMS = Set.of(InvestmentApiConstants.CONFIRMATION_RECEIVED_ON,
|
||||||
|
InvestmentApiConstants.CONFIRMATION_REF, 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> 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> 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);
|
public static final Set<String> AMORTISE_PARAMS = Set.of(PURCHASE_ID, FY_END, LOCALE, DATE_FORMAT);
|
||||||
|
|||||||
+94
@@ -0,0 +1,94 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One deal slip: a purchase (BUY) or a sale, refund, redemption or switch, with everything the slip must show — the
|
||||||
|
* counterparty, the security, amount, price, deal date and time, settlement date, how the deal was confirmed, direct or
|
||||||
|
* through a broker (and the brokerage), and who entered, approved, settled or cancelled it. A dropped deal stays on the
|
||||||
|
* register as CANCELLED so its number is accounted for.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class InvestmentDealSlipData {
|
||||||
|
|
||||||
|
/** Purchase deal number (202627000001) or sale number (202627S00001). */
|
||||||
|
private final String slipNo;
|
||||||
|
/** BUY, SELL, REFUND (shares), REDEEM or SWITCH. */
|
||||||
|
private final String side;
|
||||||
|
/** PENDING_SETTLEMENT, SETTLED or CANCELLED. */
|
||||||
|
private final String status;
|
||||||
|
/** Status of the purchase / sale record itself (ACTIVE, SOLD, DROPPED, …). */
|
||||||
|
private final String recordStatus;
|
||||||
|
private final Long purchaseId;
|
||||||
|
private final Long saleId;
|
||||||
|
/** Deal number of the holding (for a sale, the purchase it sells from). */
|
||||||
|
private final String dealNo;
|
||||||
|
private final String certNo;
|
||||||
|
private final String dealDate;
|
||||||
|
private final String dealTime;
|
||||||
|
private final String settlementType;
|
||||||
|
private final String settlementDate;
|
||||||
|
private final String partyCode;
|
||||||
|
private final String counterparty;
|
||||||
|
private final String bondType;
|
||||||
|
private final String scriptCode;
|
||||||
|
private final String scriptDesc;
|
||||||
|
private final String isin;
|
||||||
|
private final String category;
|
||||||
|
private final BigDecimal units;
|
||||||
|
private final BigDecimal faceValue;
|
||||||
|
/** Price per ₹100 of face value, NAV per unit or price per share. */
|
||||||
|
private final BigDecimal price;
|
||||||
|
/** Price of the units without interest or charges. */
|
||||||
|
private final BigDecimal consideration;
|
||||||
|
private final BigDecimal accruedInterest;
|
||||||
|
private final BigDecimal brokerage;
|
||||||
|
/** Stamp duty and other charges of a purchase. */
|
||||||
|
private final BigDecimal otherCharges;
|
||||||
|
/** Money paid out (purchase) or received (sale). */
|
||||||
|
private final BigDecimal netAmount;
|
||||||
|
private final String bankGlCode;
|
||||||
|
private final String bankGlName;
|
||||||
|
/** DIRECT or BROKER. */
|
||||||
|
private final String dealMode;
|
||||||
|
private final String brokerCode;
|
||||||
|
private final String tradeNo;
|
||||||
|
private final String orderNo;
|
||||||
|
private final String confirmationMode;
|
||||||
|
private final String confirmationReceivedOn;
|
||||||
|
private final String confirmationRef;
|
||||||
|
private final String enteredBy;
|
||||||
|
private final String enteredOn;
|
||||||
|
private final String approvedBy;
|
||||||
|
private final String approvedOn;
|
||||||
|
private final String settledBy;
|
||||||
|
private final String settledOn;
|
||||||
|
private final String settleApprovedBy;
|
||||||
|
private final String settleApprovedOn;
|
||||||
|
private final String cancelledBy;
|
||||||
|
private final String cancelledOn;
|
||||||
|
private final String voucherNo;
|
||||||
|
private final String glTransactionId;
|
||||||
|
private final String remarks;
|
||||||
|
}
|
||||||
+6
-1
@@ -72,6 +72,9 @@ public class InvestmentPurchaseData {
|
|||||||
private final String crGlCode;
|
private final String crGlCode;
|
||||||
private final String crGlName;
|
private final String crGlName;
|
||||||
private final String otherDetails;
|
private final String otherDetails;
|
||||||
|
private final String confirmationMode;
|
||||||
|
private final String confirmationReceivedOn;
|
||||||
|
private final String confirmationRef;
|
||||||
private final String status;
|
private final String status;
|
||||||
private final String glTransactionId;
|
private final String glTransactionId;
|
||||||
private final String settledOnDate;
|
private final String settledOnDate;
|
||||||
@@ -90,7 +93,9 @@ public class InvestmentPurchaseData {
|
|||||||
.subType(p.getSubType()).investmentGlAccountId(p.getInvestmentGlAccount().getId())
|
.subType(p.getSubType()).investmentGlAccountId(p.getInvestmentGlAccount().getId())
|
||||||
.investmentGlCode(p.getInvestmentGlAccount().getGlCode()).investmentGlName(p.getInvestmentGlAccount().getName())
|
.investmentGlCode(p.getInvestmentGlAccount().getGlCode()).investmentGlName(p.getInvestmentGlAccount().getName())
|
||||||
.crGlAccountId(p.getCrGlAccount().getId()).crGlCode(p.getCrGlAccount().getGlCode()).crGlName(p.getCrGlAccount().getName())
|
.crGlAccountId(p.getCrGlAccount().getId()).crGlCode(p.getCrGlAccount().getGlCode()).crGlName(p.getCrGlAccount().getName())
|
||||||
.otherDetails(p.getOtherDetails()).status(p.getStatus()).glTransactionId(p.getGlTransactionId())
|
.otherDetails(p.getOtherDetails()).confirmationMode(p.getConfirmationMode())
|
||||||
|
.confirmationReceivedOn(format(p.getConfirmationReceivedOn())).confirmationRef(p.getConfirmationRef())
|
||||||
|
.status(p.getStatus()).glTransactionId(p.getGlTransactionId())
|
||||||
.settledOnDate(format(p.getSettledOnDate())).build();
|
.settledOnDate(format(p.getSettledOnDate())).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-2
@@ -35,6 +35,8 @@ public class InvestmentSaleData {
|
|||||||
private final Long id;
|
private final Long id;
|
||||||
private final Long purchaseId;
|
private final Long purchaseId;
|
||||||
private final String dealNo;
|
private final String dealNo;
|
||||||
|
/** Deal slip number of the sale itself, e.g. 202627S00001 (empty for a write-off or amortisation). */
|
||||||
|
private final String saleNo;
|
||||||
private final String certNo;
|
private final String certNo;
|
||||||
private final String bondType;
|
private final String bondType;
|
||||||
private final String scriptCode;
|
private final String scriptCode;
|
||||||
@@ -68,13 +70,17 @@ public class InvestmentSaleData {
|
|||||||
private final String premiumGlCode;
|
private final String premiumGlCode;
|
||||||
private final String sanctionAuthority;
|
private final String sanctionAuthority;
|
||||||
private final String remarks;
|
private final String remarks;
|
||||||
|
private final String brokerCode;
|
||||||
|
private final String confirmationMode;
|
||||||
|
private final String confirmationReceivedOn;
|
||||||
|
private final String confirmationRef;
|
||||||
private final String status;
|
private final String status;
|
||||||
private final String glTransactionId;
|
private final String glTransactionId;
|
||||||
private final String voucherNo;
|
private final String voucherNo;
|
||||||
private final String settledOnDate;
|
private final String settledOnDate;
|
||||||
|
|
||||||
public static InvestmentSaleData from(final InvestmentSale s) {
|
public static InvestmentSaleData from(final InvestmentSale s) {
|
||||||
return builder().id(s.getId()).purchaseId(s.getPurchase().getId()).dealNo(s.getPurchase().getDealNo())
|
return builder().id(s.getId()).purchaseId(s.getPurchase().getId()).dealNo(s.getPurchase().getDealNo()).saleNo(s.getSaleNo())
|
||||||
.certNo(s.getPurchase().getCertNo()).bondType(s.getPurchase().getBondType())
|
.certNo(s.getPurchase().getCertNo()).bondType(s.getPurchase().getBondType())
|
||||||
.scriptCode(s.getPurchase().getScript().getScriptCode()).scriptDesc(s.getPurchase().getScript().getScriptDesc())
|
.scriptCode(s.getPurchase().getScript().getScriptCode()).scriptDesc(s.getPurchase().getScript().getScriptDesc())
|
||||||
.saleType(s.getSaleType()).partyId(s.getParty() == null ? null : s.getParty().getId())
|
.saleType(s.getSaleType()).partyId(s.getParty() == null ? null : s.getParty().getId())
|
||||||
@@ -88,7 +94,9 @@ public class InvestmentSaleData {
|
|||||||
.bankGlName(s.getBankGlAccount().getName()).investmentGlCode(s.getPurchase().getInvestmentGlAccount().getGlCode())
|
.bankGlName(s.getBankGlAccount().getName()).investmentGlCode(s.getPurchase().getInvestmentGlAccount().getGlCode())
|
||||||
.premiumGlCode(s.getPurchase().getPremiumGlAccount() != null ? s.getPurchase().getPremiumGlAccount().getGlCode()
|
.premiumGlCode(s.getPurchase().getPremiumGlAccount() != null ? s.getPurchase().getPremiumGlAccount().getGlCode()
|
||||||
: s.getPurchase().getInvestmentGlAccount().getGlCode())
|
: s.getPurchase().getInvestmentGlAccount().getGlCode())
|
||||||
.sanctionAuthority(s.getSanctionAuthority()).remarks(s.getRemarks()).status(s.getStatus())
|
.sanctionAuthority(s.getSanctionAuthority()).remarks(s.getRemarks()).brokerCode(s.getBrokerCode())
|
||||||
|
.confirmationMode(s.getConfirmationMode()).confirmationReceivedOn(format(s.getConfirmationReceivedOn()))
|
||||||
|
.confirmationRef(s.getConfirmationRef()).status(s.getStatus())
|
||||||
.glTransactionId(s.getGlTransactionId()).voucherNo(s.getVoucherNo()).settledOnDate(format(s.getSettledOnDate())).build();
|
.glTransactionId(s.getGlTransactionId()).voucherNo(s.getVoucherNo()).settledOnDate(format(s.getSettledOnDate())).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
@@ -151,6 +151,17 @@ public class InvestmentPurchase extends AbstractAuditableWithUTCDateTimeCustom<L
|
|||||||
@Column(name = "other_details", length = 4000)
|
@Column(name = "other_details", length = 4000)
|
||||||
private String otherDetails;
|
private String otherDetails;
|
||||||
|
|
||||||
|
/** How the deal was confirmed to the counterparty (EMAIL, NDS_OM, FAX, LETTER, PHONE), entered with the deal. */
|
||||||
|
@Column(name = "confirmation_mode", length = 10)
|
||||||
|
private String confirmationMode;
|
||||||
|
|
||||||
|
/** Date the counterparty's confirmation / contract note reached the back office, recorded on Settle. */
|
||||||
|
@Column(name = "confirmation_received_on")
|
||||||
|
private LocalDate confirmationReceivedOn;
|
||||||
|
|
||||||
|
@Column(name = "confirmation_ref", length = 100)
|
||||||
|
private String confirmationRef;
|
||||||
|
|
||||||
@Column(name = "status", nullable = false, length = 20)
|
@Column(name = "status", nullable = false, length = 20)
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
|
|||||||
+7
@@ -19,6 +19,8 @@
|
|||||||
package org.apache.fineract.portfolio.investment.domain;
|
package org.apache.fineract.portfolio.investment.domain;
|
||||||
|
|
||||||
import jakarta.persistence.LockModeType;
|
import jakarta.persistence.LockModeType;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.data.jpa.repository.Lock;
|
import org.springframework.data.jpa.repository.Lock;
|
||||||
@@ -35,4 +37,9 @@ public interface InvestmentPurchaseRepository extends JpaRepository<InvestmentPu
|
|||||||
boolean existsByScriptIdAndStatusNot(Long scriptId, String status);
|
boolean existsByScriptIdAndStatusNot(Long scriptId, String status);
|
||||||
|
|
||||||
boolean existsByScriptIdAndStatus(Long scriptId, String status);
|
boolean existsByScriptIdAndStatus(Long scriptId, String status);
|
||||||
|
|
||||||
|
/** Every deal dated in the period, dropped ones included (the deal slip register). */
|
||||||
|
List<InvestmentPurchase> findByDealDateBetween(LocalDate from, LocalDate to);
|
||||||
|
|
||||||
|
Optional<InvestmentPurchase> findByDealNo(String dealNo);
|
||||||
}
|
}
|
||||||
|
|||||||
+22
@@ -43,6 +43,13 @@ public class InvestmentSale extends AbstractAuditableWithUTCDateTimeCustom<Long>
|
|||||||
@JoinColumn(name = "purchase_id", nullable = false)
|
@JoinColumn(name = "purchase_id", nullable = false)
|
||||||
private InvestmentPurchase purchase;
|
private InvestmentPurchase purchase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deal slip number of a sale, redemption or switch: financial year, "S" and a running number, e.g. 202627S00001.
|
||||||
|
* Empty for a write-off or amortisation, which are not deals.
|
||||||
|
*/
|
||||||
|
@Column(name = "sale_no", length = 20, unique = true)
|
||||||
|
private String saleNo;
|
||||||
|
|
||||||
/** SELL, REDEEM, WRITEOFF or AMORTISE. */
|
/** SELL, REDEEM, WRITEOFF or AMORTISE. */
|
||||||
@Column(name = "sale_type", nullable = false, length = 10)
|
@Column(name = "sale_type", nullable = false, length = 10)
|
||||||
private String saleType;
|
private String saleType;
|
||||||
@@ -118,6 +125,21 @@ public class InvestmentSale extends AbstractAuditableWithUTCDateTimeCustom<Long>
|
|||||||
@Column(name = "remarks", length = 500)
|
@Column(name = "remarks", length = 500)
|
||||||
private String remarks;
|
private String remarks;
|
||||||
|
|
||||||
|
/** Broker of a sale done through a broker (Broker Master code); empty for a direct deal. */
|
||||||
|
@Column(name = "broker_code", length = 20)
|
||||||
|
private String brokerCode;
|
||||||
|
|
||||||
|
/** How the deal was confirmed to the counterparty (EMAIL, NDS_OM, FAX, LETTER, PHONE), entered with the sale. */
|
||||||
|
@Column(name = "confirmation_mode", length = 10)
|
||||||
|
private String confirmationMode;
|
||||||
|
|
||||||
|
/** Date the counterparty's confirmation / contract note reached the back office, recorded on Settle. */
|
||||||
|
@Column(name = "confirmation_received_on")
|
||||||
|
private LocalDate confirmationReceivedOn;
|
||||||
|
|
||||||
|
@Column(name = "confirmation_ref", length = 100)
|
||||||
|
private String confirmationRef;
|
||||||
|
|
||||||
@Column(name = "status", nullable = false, length = 20)
|
@Column(name = "status", nullable = false, length = 20)
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
|
|||||||
+6
@@ -19,6 +19,7 @@
|
|||||||
package org.apache.fineract.portfolio.investment.domain;
|
package org.apache.fineract.portfolio.investment.domain;
|
||||||
|
|
||||||
import jakarta.persistence.LockModeType;
|
import jakarta.persistence.LockModeType;
|
||||||
|
import java.time.LocalDate;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
@@ -34,6 +35,11 @@ public interface InvestmentSaleRepository extends JpaRepository<InvestmentSale,
|
|||||||
@Query("select s from InvestmentSale s where s.id = :saleId")
|
@Query("select s from InvestmentSale s where s.id = :saleId")
|
||||||
Optional<InvestmentSale> findByIdWithLock(@Param("saleId") Long saleId);
|
Optional<InvestmentSale> findByIdWithLock(@Param("saleId") Long saleId);
|
||||||
|
|
||||||
|
/** Every numbered sale, redemption and switch dated in the period, dropped ones included (the deal slip register). */
|
||||||
|
List<InvestmentSale> findBySaleNoNotNullAndDealDateBetween(LocalDate from, LocalDate to);
|
||||||
|
|
||||||
|
Optional<InvestmentSale> findBySaleNo(String saleNo);
|
||||||
|
|
||||||
/** The deal of a sale, without loading either, so the deal can be locked first. */
|
/** The deal of a sale, without loading either, so the deal can be locked first. */
|
||||||
@Query("select s.purchase.id from InvestmentSale s where s.id = :saleId")
|
@Query("select s.purchase.id from InvestmentSale s where s.id = :saleId")
|
||||||
Optional<Long> findPurchaseIdBySaleId(@Param("saleId") Long saleId);
|
Optional<Long> findPurchaseIdBySaleId(@Param("saleId") Long saleId);
|
||||||
|
|||||||
+26
@@ -38,6 +38,7 @@ import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApi
|
|||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SCRIPT_ID;
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SCRIPT_ID;
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SETTLEMENT_TYPE;
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SETTLEMENT_TYPE;
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SETTLEMENT_TYPES;
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SETTLEMENT_TYPES;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SETTLE_PARAMS;
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.STAMP_DUTY;
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.STAMP_DUTY;
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SUB_TYPE;
|
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SUB_TYPE;
|
||||||
|
|
||||||
@@ -122,6 +123,11 @@ public final class InvestmentPurchaseCommandFromApiJsonDeserializer {
|
|||||||
}
|
}
|
||||||
v.reset().parameter(SUB_TYPE).value(this.fromApiJsonHelper.extractStringNamed(SUB_TYPE, element)).ignoreIfNull()
|
v.reset().parameter(SUB_TYPE).value(this.fromApiJsonHelper.extractStringNamed(SUB_TYPE, element)).ignoreIfNull()
|
||||||
.notExceedingLengthOf(40);
|
.notExceedingLengthOf(40);
|
||||||
|
final String confirmationMode = this.fromApiJsonHelper.extractStringNamed(InvestmentApiConstants.CONFIRMATION_MODE, element);
|
||||||
|
if (StringUtils.isNotBlank(confirmationMode)) {
|
||||||
|
v.reset().parameter(InvestmentApiConstants.CONFIRMATION_MODE).value(confirmationMode)
|
||||||
|
.isOneOfTheseValues(InvestmentApiConstants.CONFIRMATION_MODES.toArray());
|
||||||
|
}
|
||||||
|
|
||||||
if (this.fromApiJsonHelper.parameterExists(OTHER_DETAILS, element)) {
|
if (this.fromApiJsonHelper.parameterExists(OTHER_DETAILS, element)) {
|
||||||
final JsonElement other = element.getAsJsonObject().get(OTHER_DETAILS);
|
final JsonElement other = element.getAsJsonObject().get(OTHER_DETAILS);
|
||||||
@@ -137,6 +143,26 @@ public final class InvestmentPurchaseCommandFromApiJsonDeserializer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Settle may record the counterparty's confirmation of the deal: the date it was received and its reference. */
|
||||||
|
public void validateForSettle(final String json) {
|
||||||
|
if (StringUtils.isBlank(json)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
||||||
|
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, SETTLE_PARAMS);
|
||||||
|
final JsonElement element = this.fromApiJsonHelper.parse(json);
|
||||||
|
final List<ApiParameterError> errors = new ArrayList<>();
|
||||||
|
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(RESOURCE);
|
||||||
|
// A malformed date fails here; the dates it may take are checked against the deal.
|
||||||
|
this.fromApiJsonHelper.extractLocalDateNamed(InvestmentApiConstants.CONFIRMATION_RECEIVED_ON, element);
|
||||||
|
v.reset().parameter(InvestmentApiConstants.CONFIRMATION_REF)
|
||||||
|
.value(this.fromApiJsonHelper.extractStringNamed(InvestmentApiConstants.CONFIRMATION_REF, element)).ignoreIfNull()
|
||||||
|
.notExceedingLengthOf(100);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private BigDecimal amount(final String param, final JsonElement element) {
|
private BigDecimal amount(final String param, final JsonElement element) {
|
||||||
return this.fromApiJsonHelper.parameterExists(param, element)
|
return this.fromApiJsonHelper.parameterExists(param, element)
|
||||||
? this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(param, element)
|
? this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(param, element)
|
||||||
|
|||||||
+26
@@ -21,6 +21,7 @@ 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.AMOUNT;
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.BANK_GL_ACCOUNT_ID;
|
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.BROKERAGE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.BROKER_CODE;
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.DEAL_DATE;
|
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.PARTY_ID;
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.PURCHASE_ID;
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.PURCHASE_ID;
|
||||||
@@ -31,6 +32,7 @@ import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiCons
|
|||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.SANCTION_AUTHORITY;
|
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.SELL_PARAMS;
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.SETTLEMENT_TYPE;
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.SETTLEMENT_TYPE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.SETTLE_PARAMS;
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.UNITS;
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.UNITS;
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.WRITEOFF_PARAMS;
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.WRITEOFF_PARAMS;
|
||||||
|
|
||||||
@@ -48,6 +50,7 @@ import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
|||||||
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
|
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
|
||||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||||
|
import org.apache.fineract.portfolio.investment.api.InvestmentApiConstants;
|
||||||
import org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants;
|
import org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@@ -84,6 +87,29 @@ public final class InvestmentSaleCommandFromApiJsonDeserializer {
|
|||||||
.ignoreIfNull().notExceedingLengthOf(100);
|
.ignoreIfNull().notExceedingLengthOf(100);
|
||||||
v.reset().parameter(REMARKS).value(this.fromApiJsonHelper.extractStringNamed(REMARKS, element)).ignoreIfNull()
|
v.reset().parameter(REMARKS).value(this.fromApiJsonHelper.extractStringNamed(REMARKS, element)).ignoreIfNull()
|
||||||
.notExceedingLengthOf(500);
|
.notExceedingLengthOf(500);
|
||||||
|
v.reset().parameter(BROKER_CODE).value(this.fromApiJsonHelper.extractStringNamed(BROKER_CODE, element)).ignoreIfNull()
|
||||||
|
.notExceedingLengthOf(20);
|
||||||
|
final String confirmationMode = this.fromApiJsonHelper.extractStringNamed(InvestmentApiConstants.CONFIRMATION_MODE, element);
|
||||||
|
if (StringUtils.isNotBlank(confirmationMode)) {
|
||||||
|
v.reset().parameter(InvestmentApiConstants.CONFIRMATION_MODE).value(confirmationMode)
|
||||||
|
.isOneOfTheseValues(InvestmentApiConstants.CONFIRMATION_MODES.toArray());
|
||||||
|
}
|
||||||
|
throwIfAny(errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Settle may record the counterparty's confirmation of the deal: the date it was received and its reference. */
|
||||||
|
public void validateForSettle(final String json) {
|
||||||
|
if (StringUtils.isBlank(json)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final JsonElement element = parse(json, SETTLE_PARAMS);
|
||||||
|
final List<ApiParameterError> errors = new ArrayList<>();
|
||||||
|
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(RESOURCE);
|
||||||
|
// A malformed date fails here; the dates it may take are checked against the deal.
|
||||||
|
this.fromApiJsonHelper.extractLocalDateNamed(InvestmentApiConstants.CONFIRMATION_RECEIVED_ON, element);
|
||||||
|
v.reset().parameter(InvestmentApiConstants.CONFIRMATION_REF)
|
||||||
|
.value(this.fromApiJsonHelper.extractStringNamed(InvestmentApiConstants.CONFIRMATION_REF, element)).ignoreIfNull()
|
||||||
|
.notExceedingLengthOf(100);
|
||||||
throwIfAny(errors);
|
throwIfAny(errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+254
@@ -0,0 +1,254 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.service;
|
||||||
|
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
|
import org.apache.fineract.portfolio.investment.api.InvestmentApiConstants;
|
||||||
|
import org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants;
|
||||||
|
import org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants;
|
||||||
|
import org.apache.fineract.portfolio.investment.data.InvestmentDealSlipData;
|
||||||
|
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.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deal slips and the deal slip register: every purchase and every sale, refund, redemption and switch in serial order,
|
||||||
|
* dropped ones included as cancelled, with who entered, approved, settled or cancelled each (from the maker-checker
|
||||||
|
* record of its commands).
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public class InvestmentDealSlipService {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
||||||
|
private static final DateTimeFormatter DATE_TIME = DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm");
|
||||||
|
private static final String PURCHASE = InvestmentPurchaseApiConstants.ENTITY;
|
||||||
|
private static final String SALE = InvestmentSaleApiConstants.ENTITY;
|
||||||
|
/** Command processed (not awaiting approval, rejected or failed). */
|
||||||
|
private static final int PROCESSED = 1;
|
||||||
|
|
||||||
|
private final InvestmentPurchaseRepository purchaseRepository;
|
||||||
|
private final InvestmentSaleRepository saleRepository;
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
|
||||||
|
/** Who made (and who approved) one command on a deal. */
|
||||||
|
private record Action(String maker, String madeOn, String checker, String checkedOn) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Slips of deals dated from {@code from} to {@code to}, in slip number order. */
|
||||||
|
public List<InvestmentDealSlipData> register(final LocalDate from, final LocalDate to) {
|
||||||
|
return slips(this.purchaseRepository.findByDealDateBetween(from, to),
|
||||||
|
this.saleRepository.findBySaleNoNotNullAndDealDateBetween(from, to));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One slip by its number: a purchase deal number or a sale number; empty when there is none. */
|
||||||
|
public List<InvestmentDealSlipData> slip(final String slipNo) {
|
||||||
|
final String no = slipNo.trim();
|
||||||
|
return slips(this.purchaseRepository.findByDealNo(no).stream().toList(), this.saleRepository.findBySaleNo(no).stream().toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<InvestmentDealSlipData> slips(final List<InvestmentPurchase> purchases, final List<InvestmentSale> sales) {
|
||||||
|
final Map<String, Action> actions = actions(purchases.stream().map(InvestmentPurchase::getId).toList(),
|
||||||
|
sales.stream().map(InvestmentSale::getId).toList());
|
||||||
|
final List<InvestmentDealSlipData> slips = new ArrayList<>();
|
||||||
|
purchases.forEach(p -> slips.add(purchaseSlip(p, actions)));
|
||||||
|
sales.forEach(s -> slips.add(saleSlip(s, actions)));
|
||||||
|
slips.sort(Comparator.comparing(InvestmentDealSlipData::getSlipNo));
|
||||||
|
return slips;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InvestmentDealSlipData purchaseSlip(final InvestmentPurchase p, final Map<String, Action> actions) {
|
||||||
|
final JsonObject other = otherDetails(p.getOtherDetails());
|
||||||
|
final String brokerCode = text(other, "brokerCode");
|
||||||
|
// A purchase opened by a mutual fund switch has no CREATE of its own: the switch made it.
|
||||||
|
final Action entered = actions.getOrDefault(key(PURCHASE, "CREATE", p.getId()), actions.get(key(SALE, "SWITCH", p.getId())));
|
||||||
|
final Action settled = actions.getOrDefault(key(PURCHASE, "SETTLE", p.getId()), actions.get(key(SALE, "SWITCH", p.getId())));
|
||||||
|
final Action cancelled = actions.get(key(PURCHASE, "DELETE", p.getId()));
|
||||||
|
final String status = InvestmentPurchaseApiConstants.STATUS_DROPPED.equals(p.getStatus()) ? "CANCELLED"
|
||||||
|
: InvestmentPurchaseApiConstants.STATUS_PENDING_SETTLEMENT.equals(p.getStatus()) ? "PENDING_SETTLEMENT" : "SETTLED";
|
||||||
|
final InvestmentDealSlipData.InvestmentDealSlipDataBuilder b = InvestmentDealSlipData.builder().slipNo(p.getDealNo()).side("BUY")
|
||||||
|
.status(status).recordStatus(p.getStatus()).purchaseId(p.getId()).dealNo(p.getDealNo()).certNo(p.getCertNo())
|
||||||
|
.dealDate(format(p.getDealDate())).dealTime(p.getDealTime()).settlementType(p.getSettlementType())
|
||||||
|
.settlementDate(format(p.getSettlementDate())).partyCode(p.getParty().getPartyCode())
|
||||||
|
.counterparty(p.getParty().getPartyName()).bondType(p.getBondType()).scriptCode(p.getScript().getScriptCode())
|
||||||
|
.scriptDesc(p.getScript().getScriptDesc()).isin(p.getScript().getIsin()).category(p.getCategory()).units(p.getUnits())
|
||||||
|
.faceValue(p.getFaceValue()).price(p.getPurchaseRate()).consideration(p.getBookValue())
|
||||||
|
.accruedInterest(p.getAccruedInterest()).brokerage(p.getBrokerage())
|
||||||
|
.otherCharges(zero(p.getStampDuty()).add(zero(p.getOtherCharges()))).netAmount(p.getSettlementAmount())
|
||||||
|
.bankGlCode(p.getCrGlAccount().getGlCode()).bankGlName(p.getCrGlAccount().getName())
|
||||||
|
.dealMode(brokerCode == null ? "DIRECT" : "BROKER").brokerCode(brokerCode).tradeNo(text(other, "tradeNo"))
|
||||||
|
.orderNo(text(other, "orderNo")).confirmationMode(p.getConfirmationMode())
|
||||||
|
.confirmationReceivedOn(format(p.getConfirmationReceivedOn())).confirmationRef(p.getConfirmationRef())
|
||||||
|
.glTransactionId(p.getGlTransactionId());
|
||||||
|
return who(b, entered, settled, cancelled).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InvestmentDealSlipData saleSlip(final InvestmentSale s, final Map<String, Action> actions) {
|
||||||
|
final InvestmentPurchase p = s.getPurchase();
|
||||||
|
final String type = s.getSaleType();
|
||||||
|
final Action entered;
|
||||||
|
final Action settled;
|
||||||
|
if (InvestmentSaleApiConstants.TYPE_SWITCH.equals(type)) {
|
||||||
|
entered = actions.get(key(SALE, "SWITCH-OUT", s.getId()));
|
||||||
|
settled = entered;
|
||||||
|
} else if (InvestmentSaleApiConstants.TYPE_REDEEM.equals(type)) {
|
||||||
|
entered = actions.get(key(SALE, "REDEEM", s.getId()));
|
||||||
|
settled = entered;
|
||||||
|
} else {
|
||||||
|
entered = actions.get(key(SALE, "CREATE", s.getId()));
|
||||||
|
settled = actions.get(key(SALE, "SETTLE", s.getId()));
|
||||||
|
}
|
||||||
|
final Action cancelled = actions.get(key(SALE, "DELETE", s.getId()));
|
||||||
|
final String side = InvestmentSaleApiConstants.TYPE_SELL.equals(type)
|
||||||
|
? InvestmentApiConstants.SHARE.equals(p.getBondType()) ? "REFUND" : "SELL"
|
||||||
|
: type;
|
||||||
|
final String status = InvestmentSaleApiConstants.STATUS_DROPPED.equals(s.getStatus()) ? "CANCELLED" : s.getStatus();
|
||||||
|
// A redemption is repaid by the issuer, not bought by a counterparty.
|
||||||
|
final String counterparty = s.getParty() != null ? s.getParty().getPartyName()
|
||||||
|
: InvestmentSaleApiConstants.TYPE_REDEEM.equals(type) ? "Issuer (redemption at maturity)" : null;
|
||||||
|
final InvestmentDealSlipData.InvestmentDealSlipDataBuilder b = InvestmentDealSlipData.builder().slipNo(s.getSaleNo()).side(side)
|
||||||
|
.status(status).recordStatus(s.getStatus()).purchaseId(p.getId()).saleId(s.getId()).dealNo(p.getDealNo())
|
||||||
|
.certNo(p.getCertNo()).dealDate(format(s.getDealDate())).dealTime(s.getDealTime())
|
||||||
|
.settlementType(s.getSettlementType()).settlementDate(format(s.getSettlementDate()))
|
||||||
|
.partyCode(s.getParty() == null ? null : s.getParty().getPartyCode()).counterparty(counterparty)
|
||||||
|
.bondType(p.getBondType()).scriptCode(p.getScript().getScriptCode()).scriptDesc(p.getScript().getScriptDesc())
|
||||||
|
.isin(p.getScript().getIsin()).category(p.getCategory()).units(s.getUnits()).faceValue(s.getFaceValue())
|
||||||
|
.price(s.getSaleRate()).consideration(s.getConsideration()).accruedInterest(s.getAccruedInterest())
|
||||||
|
.brokerage(s.getBrokerage()).otherCharges(BigDecimal.ZERO).netAmount(s.getNetAmount())
|
||||||
|
.bankGlCode(s.getBankGlAccount().getGlCode()).bankGlName(s.getBankGlAccount().getName())
|
||||||
|
.dealMode(StringUtils.isBlank(s.getBrokerCode()) ? "DIRECT" : "BROKER").brokerCode(s.getBrokerCode())
|
||||||
|
.confirmationMode(s.getConfirmationMode()).confirmationReceivedOn(format(s.getConfirmationReceivedOn()))
|
||||||
|
.confirmationRef(s.getConfirmationRef()).voucherNo(s.getVoucherNo()).glTransactionId(s.getGlTransactionId())
|
||||||
|
.remarks(s.getRemarks());
|
||||||
|
return who(b, entered, settled, cancelled).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InvestmentDealSlipData.InvestmentDealSlipDataBuilder who(final InvestmentDealSlipData.InvestmentDealSlipDataBuilder b,
|
||||||
|
final Action entered, final Action settled, final Action cancelled) {
|
||||||
|
if (entered != null) {
|
||||||
|
b.enteredBy(entered.maker()).enteredOn(entered.madeOn()).approvedBy(entered.checker()).approvedOn(entered.checkedOn());
|
||||||
|
}
|
||||||
|
if (settled != null) {
|
||||||
|
b.settledBy(settled.maker()).settledOn(settled.madeOn()).settleApprovedBy(settled.checker())
|
||||||
|
.settleApprovedOn(settled.checkedOn());
|
||||||
|
}
|
||||||
|
if (cancelled != null) {
|
||||||
|
b.cancelledBy(cancelled.maker()).cancelledOn(cancelled.madeOn());
|
||||||
|
}
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The processed commands on these deals, keyed entity|action|id. A switch is recorded against the deal it opens, with
|
||||||
|
* the switched-out sale as its sub-resource, so it is keyed both ways.
|
||||||
|
*/
|
||||||
|
private Map<String, Action> actions(final Collection<Long> purchaseIds, final Collection<Long> saleIds) {
|
||||||
|
final Map<String, Action> actions = new HashMap<>();
|
||||||
|
if (purchaseIds.isEmpty() && saleIds.isEmpty()) {
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
final String purchases = ids(purchaseIds);
|
||||||
|
final String sales = ids(saleIds);
|
||||||
|
final String sql = """
|
||||||
|
SELECT c.entity_name, c.action_name, c.resource_id, c.subresource_id, c.made_on_date_utc, c.checked_on_date_utc,
|
||||||
|
mk.username AS maker, ck.username AS checker
|
||||||
|
FROM m_portfolio_command_source c
|
||||||
|
LEFT JOIN m_appuser mk ON mk.id = c.maker_id
|
||||||
|
LEFT JOIN m_appuser ck ON ck.id = c.checker_id
|
||||||
|
WHERE c.status = ?
|
||||||
|
AND ((c.entity_name = 'INVESTMENTPURCHASE' AND c.resource_id IN (%1$s))
|
||||||
|
OR (c.entity_name = 'INVESTMENTSALE' AND (c.resource_id IN (%2$s) OR c.resource_id IN (%1$s)
|
||||||
|
OR c.subresource_id IN (%2$s))))
|
||||||
|
ORDER BY c.id
|
||||||
|
""".formatted(purchases, sales);
|
||||||
|
this.jdbcTemplate.query(sql, rs -> {
|
||||||
|
final String entity = rs.getString("entity_name");
|
||||||
|
final String action = rs.getString("action_name");
|
||||||
|
final long resourceId = rs.getLong("resource_id");
|
||||||
|
final long subResourceId = rs.getLong("subresource_id");
|
||||||
|
final Action a = new Action(rs.getString("maker"), dateTime(rs.getObject("made_on_date_utc", OffsetDateTime.class)),
|
||||||
|
rs.getString("checker"), dateTime(rs.getObject("checked_on_date_utc", OffsetDateTime.class)));
|
||||||
|
// Later rows (a retry after a rejection) replace earlier ones.
|
||||||
|
actions.put(key(entity, action, resourceId), a);
|
||||||
|
if (SALE.equals(entity) && "SWITCH".equals(action)) {
|
||||||
|
actions.put(key(SALE, "SWITCH-OUT", subResourceId), a);
|
||||||
|
}
|
||||||
|
}, PROCESSED);
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ids for an IN list; never empty, so the SQL stays valid. */
|
||||||
|
private static String ids(final Collection<Long> ids) {
|
||||||
|
return ids.isEmpty() ? "-1" : ids.stream().map(String::valueOf).collect(Collectors.joining(","));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String key(final String entity, final String action, final long id) {
|
||||||
|
return entity + "|" + action + "|" + id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonObject otherDetails(final String json) {
|
||||||
|
if (StringUtils.isBlank(json)) {
|
||||||
|
return new JsonObject();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final JsonElement e = JsonParser.parseString(json);
|
||||||
|
return e.isJsonObject() ? e.getAsJsonObject() : new JsonObject();
|
||||||
|
} catch (final RuntimeException e) {
|
||||||
|
return new JsonObject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String text(final JsonObject o, final String name) {
|
||||||
|
final JsonElement e = o.get(name);
|
||||||
|
return e == null || e.isJsonNull() ? null : StringUtils.trimToNull(e.getAsString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String dateTime(final OffsetDateTime at) {
|
||||||
|
return at == null ? null : at.atZoneSameInstant(DateUtils.getDateTimeZoneOfTenant()).format(DATE_TIME);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String format(final LocalDate date) {
|
||||||
|
return date == null ? null : date.format(DATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BigDecimal zero(final BigDecimal v) {
|
||||||
|
return v == null ? BigDecimal.ZERO : v;
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
@@ -166,6 +166,7 @@ public class InvestmentFundSwitchService {
|
|||||||
|
|
||||||
final InvestmentSale s = new InvestmentSale();
|
final InvestmentSale s = new InvestmentSale();
|
||||||
s.setPurchase(p);
|
s.setPurchase(p);
|
||||||
|
s.setSaleNo(InvestmentPurchaseWritePlatformServiceImpl.nextSaleNo(this.jdbcTemplate, switchDate));
|
||||||
s.setSaleType(TYPE_SWITCH);
|
s.setSaleType(TYPE_SWITCH);
|
||||||
s.setParty(p.getParty());
|
s.setParty(p.getParty());
|
||||||
s.setDealDate(switchDate);
|
s.setDealDate(switchDate);
|
||||||
|
|||||||
+42
@@ -185,6 +185,8 @@ public class InvestmentPurchaseWritePlatformServiceImpl implements InvestmentPur
|
|||||||
p.setInvestmentGlAccount(script.getGlAccount());
|
p.setInvestmentGlAccount(script.getGlAccount());
|
||||||
p.setCrGlAccount(postableAssetGl(command.longValueOfParameterNamed(CR_GL_ACCOUNT_ID)));
|
p.setCrGlAccount(postableAssetGl(command.longValueOfParameterNamed(CR_GL_ACCOUNT_ID)));
|
||||||
p.setOtherDetails(otherDetails(command));
|
p.setOtherDetails(otherDetails(command));
|
||||||
|
p.setConfirmationMode(
|
||||||
|
StringUtils.trimToNull(command.stringValueOfParameterNamedAllowingNull(InvestmentApiConstants.CONFIRMATION_MODE)));
|
||||||
p.setStatus(STATUS_PENDING_SETTLEMENT);
|
p.setStatus(STATUS_PENDING_SETTLEMENT);
|
||||||
|
|
||||||
// Brokerage, stamp duty and other charges are expenses of the deal (Jitendra Sir): paid with the
|
// Brokerage, stamp duty and other charges are expenses of the deal (Jitendra Sir): paid with the
|
||||||
@@ -250,11 +252,15 @@ public class InvestmentPurchaseWritePlatformServiceImpl implements InvestmentPur
|
|||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public CommandProcessingResult settlePurchase(final Long purchaseId, final JsonCommand command) {
|
public CommandProcessingResult settlePurchase(final Long purchaseId, final JsonCommand command) {
|
||||||
|
this.validator.validateForSettle(command.json());
|
||||||
final InvestmentPurchase p = this.purchaseRepository.findByIdWithLock(purchaseId)
|
final InvestmentPurchase p = this.purchaseRepository.findByIdWithLock(purchaseId)
|
||||||
.orElseThrow(() -> new InvestmentPurchaseNotFoundException(purchaseId));
|
.orElseThrow(() -> new InvestmentPurchaseNotFoundException(purchaseId));
|
||||||
if (!STATUS_PENDING_SETTLEMENT.equals(p.getStatus())) {
|
if (!STATUS_PENDING_SETTLEMENT.equals(p.getStatus())) {
|
||||||
throw rule("settle.not.pending", "Deal " + p.getDealNo() + " is not pending settlement");
|
throw rule("settle.not.pending", "Deal " + p.getDealNo() + " is not pending settlement");
|
||||||
}
|
}
|
||||||
|
p.setConfirmationReceivedOn(confirmationReceivedOn(command, p.getDealDate()));
|
||||||
|
p.setConfirmationRef(
|
||||||
|
StringUtils.trimToNull(command.stringValueOfParameterNamedAllowingNull(InvestmentApiConstants.CONFIRMATION_REF)));
|
||||||
final LocalDate today = DateUtils.getBusinessLocalDate();
|
final LocalDate today = DateUtils.getBusinessLocalDate();
|
||||||
if (p.getSettlementDate().isAfter(today)) {
|
if (p.getSettlementDate().isAfter(today)) {
|
||||||
throw rule("settle.before.date", "Deal " + p.getDealNo() + " can be settled on or after its settlement date "
|
throw rule("settle.before.date", "Deal " + p.getDealNo() + " can be settled on or after its settlement date "
|
||||||
@@ -420,6 +426,19 @@ public class InvestmentPurchaseWritePlatformServiceImpl implements InvestmentPur
|
|||||||
return prefix + String.format("%06d", (max == null ? 0 : max) + 1);
|
return prefix + String.format("%06d", (max == null ? 0 : max) + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deal slip number of a sale, redemption or switch: FY start year + FY end (2 digits) + "S" + 5-digit running number,
|
||||||
|
* e.g. 202627S00001, so sales are numbered apart from purchases.
|
||||||
|
*/
|
||||||
|
static String nextSaleNo(final JdbcTemplate jdbcTemplate, final LocalDate dealDate) {
|
||||||
|
lockNumbering(jdbcTemplate, "m_investment_sale.sale_no");
|
||||||
|
final String prefix = dealNoPrefix(dealDate) + "S";
|
||||||
|
final Integer max = jdbcTemplate.queryForObject(
|
||||||
|
"SELECT MAX(CAST(SUBSTRING(sale_no FROM 8) AS INTEGER)) FROM m_investment_sale WHERE sale_no LIKE ?", Integer.class,
|
||||||
|
prefix + "%");
|
||||||
|
return prefix + String.format("%05d", (max == null ? 0 : max) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
/** Financial year (April–March) of the deal date: 2026-04-01 … 2027-03-31 → "202627". */
|
/** Financial year (April–March) of the deal date: 2026-04-01 … 2027-03-31 → "202627". */
|
||||||
static String dealNoPrefix(final LocalDate dealDate) {
|
static String dealNoPrefix(final LocalDate dealDate) {
|
||||||
final int fyStart = dealDate.getMonthValue() >= 4 ? dealDate.getYear() : dealDate.getYear() - 1;
|
final int fyStart = dealDate.getMonthValue() >= 4 ? dealDate.getYear() : dealDate.getYear() - 1;
|
||||||
@@ -462,6 +481,29 @@ public class InvestmentPurchaseWritePlatformServiceImpl implements InvestmentPur
|
|||||||
return value == null ? BigDecimal.ZERO : value;
|
return value == null ? BigDecimal.ZERO : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Date the counterparty's confirmation of a deal reached the back office, given on Settle (optional): not before the
|
||||||
|
* deal date and not after today.
|
||||||
|
*/
|
||||||
|
static LocalDate confirmationReceivedOn(final JsonCommand command, final LocalDate dealDate) {
|
||||||
|
final LocalDate on = command.parameterExists(InvestmentApiConstants.CONFIRMATION_RECEIVED_ON)
|
||||||
|
? command.localDateValueOfParameterNamed(InvestmentApiConstants.CONFIRMATION_RECEIVED_ON)
|
||||||
|
: null;
|
||||||
|
if (on == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final DateTimeFormatter ddMmYyyy = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
||||||
|
if (on.isBefore(dealDate)) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.investment.confirmation.before.deal",
|
||||||
|
"The confirmation cannot be received before the deal date " + dealDate.format(ddMmYyyy));
|
||||||
|
}
|
||||||
|
if (on.isAfter(DateUtils.getBusinessLocalDate())) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.investment.confirmation.future",
|
||||||
|
"The confirmation received date cannot be after today");
|
||||||
|
}
|
||||||
|
return on;
|
||||||
|
}
|
||||||
|
|
||||||
private static GeneralPlatformDomainRuleException rule(final String code, final String message) {
|
private static GeneralPlatformDomainRuleException rule(final String code, final String message) {
|
||||||
return new GeneralPlatformDomainRuleException("error.msg.investmentpurchase." + code, message);
|
return new GeneralPlatformDomainRuleException("error.msg.investmentpurchase." + code, message);
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -22,6 +22,7 @@ import java.math.BigDecimal;
|
|||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants;
|
||||||
import org.apache.fineract.portfolio.investment.data.InvestmentSaleData;
|
import org.apache.fineract.portfolio.investment.data.InvestmentSaleData;
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchase;
|
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchase;
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchaseRepository;
|
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchaseRepository;
|
||||||
@@ -40,9 +41,10 @@ public class InvestmentSaleReadPlatformService {
|
|||||||
private final InvestmentPurchaseRepository purchaseRepository;
|
private final InvestmentPurchaseRepository purchaseRepository;
|
||||||
private final InvestmentSaleQuoter quoter;
|
private final InvestmentSaleQuoter quoter;
|
||||||
|
|
||||||
/** Sales and redemptions, newest first — pending ones included. */
|
/** Sales and redemptions, newest first — pending ones included, dropped ones left out. */
|
||||||
public List<InvestmentSaleData> retrieveAll() {
|
public List<InvestmentSaleData> retrieveAll() {
|
||||||
return this.saleRepository.findAll(Sort.by(Sort.Direction.DESC, "id")).stream().map(InvestmentSaleData::from).toList();
|
return this.saleRepository.findAll(Sort.by(Sort.Direction.DESC, "id")).stream()
|
||||||
|
.filter(s -> !InvestmentSaleApiConstants.STATUS_DROPPED.equals(s.getStatus())).map(InvestmentSaleData::from).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** What a sale would come to, before it is saved. */
|
/** What a sale would come to, before it is saved. */
|
||||||
|
|||||||
+25
-4
@@ -24,6 +24,7 @@ import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApi
|
|||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.AMOUNT;
|
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.BANK_GL_ACCOUNT_ID;
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.BROKERAGE;
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.BROKERAGE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.BROKER_CODE;
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.DEAL_DATE;
|
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.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.LOSS_ON_SALE_GL_CODE;
|
||||||
@@ -35,6 +36,7 @@ import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiCons
|
|||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.SALE_RATE;
|
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.SANCTION_AUTHORITY;
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.SETTLEMENT_TYPE;
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.SETTLEMENT_TYPE;
|
||||||
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.STATUS_DROPPED;
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.STATUS_PENDING_SETTLEMENT;
|
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.STATUS_SETTLED;
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.TYPE_WRITEOFF;
|
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.TYPE_WRITEOFF;
|
||||||
@@ -45,6 +47,7 @@ import java.math.RoundingMode;
|
|||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||||
@@ -60,6 +63,7 @@ import org.apache.fineract.portfolio.investment.domain.InvestmentSaleRepository;
|
|||||||
import org.apache.fineract.portfolio.investment.exception.InvestmentPartyNotFoundException;
|
import org.apache.fineract.portfolio.investment.exception.InvestmentPartyNotFoundException;
|
||||||
import org.apache.fineract.portfolio.investment.exception.InvestmentPurchaseNotFoundException;
|
import org.apache.fineract.portfolio.investment.exception.InvestmentPurchaseNotFoundException;
|
||||||
import org.apache.fineract.portfolio.investment.serialization.InvestmentSaleCommandFromApiJsonDeserializer;
|
import org.apache.fineract.portfolio.investment.serialization.InvestmentSaleCommandFromApiJsonDeserializer;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -86,13 +90,19 @@ public class InvestmentSaleWritePlatformServiceImpl implements InvestmentSaleWri
|
|||||||
private final InvestmentPurchaseRepository purchaseRepository;
|
private final InvestmentPurchaseRepository purchaseRepository;
|
||||||
private final InvestmentPartyRepository partyRepository;
|
private final InvestmentPartyRepository partyRepository;
|
||||||
private final InvestmentJournalPoster journalPoster;
|
private final InvestmentJournalPoster journalPoster;
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public CommandProcessingResult createSale(final JsonCommand command) {
|
public CommandProcessingResult createSale(final JsonCommand command) {
|
||||||
this.validator.validateForSell(command.json());
|
this.validator.validateForSell(command.json());
|
||||||
final InvestmentPurchase p = purchase(command.longValueOfParameterNamed(PURCHASE_ID));
|
final InvestmentPurchase p = purchase(command.longValueOfParameterNamed(PURCHASE_ID));
|
||||||
final InvestmentSale sale = this.saleRepository.saveAndFlush(this.quoter.quoteSale(p, sellRequest(command), null, false));
|
final InvestmentSale quoted = this.quoter.quoteSale(p, sellRequest(command), null, false);
|
||||||
|
quoted.setSaleNo(InvestmentPurchaseWritePlatformServiceImpl.nextSaleNo(this.jdbcTemplate, quoted.getDealDate()));
|
||||||
|
quoted.setBrokerCode(StringUtils.trimToNull(command.stringValueOfParameterNamedAllowingNull(BROKER_CODE)));
|
||||||
|
quoted.setConfirmationMode(
|
||||||
|
StringUtils.trimToNull(command.stringValueOfParameterNamedAllowingNull(InvestmentApiConstants.CONFIRMATION_MODE)));
|
||||||
|
final InvestmentSale sale = this.saleRepository.saveAndFlush(quoted);
|
||||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(sale.getId())
|
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(sale.getId())
|
||||||
.withSubEntityId(p.getId()).withResourceIdAsString(p.getDealNo()).build();
|
.withSubEntityId(p.getId()).withResourceIdAsString(p.getDealNo()).build();
|
||||||
}
|
}
|
||||||
@@ -100,6 +110,7 @@ public class InvestmentSaleWritePlatformServiceImpl implements InvestmentSaleWri
|
|||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public CommandProcessingResult settleSale(final Long saleId, final JsonCommand command) {
|
public CommandProcessingResult settleSale(final Long saleId, final JsonCommand command) {
|
||||||
|
this.validator.validateForSettle(command.json());
|
||||||
// Lock the deal before the sale, as every other posting on the deal does, so two settlements cannot interleave.
|
// Lock the deal before the sale, as every other posting on the deal does, so two settlements cannot interleave.
|
||||||
final InvestmentPurchase p = purchase(this.saleRepository.findPurchaseIdBySaleId(saleId)
|
final InvestmentPurchase p = purchase(this.saleRepository.findPurchaseIdBySaleId(saleId)
|
||||||
.orElseThrow(() -> InvestmentSaleQuoter.rule("sale.not.found", "Investment sale " + saleId + " does not exist")));
|
.orElseThrow(() -> InvestmentSaleQuoter.rule("sale.not.found", "Investment sale " + saleId + " does not exist")));
|
||||||
@@ -129,6 +140,10 @@ public class InvestmentSaleWritePlatformServiceImpl implements InvestmentSaleWri
|
|||||||
lines);
|
lines);
|
||||||
|
|
||||||
copyFigures(s, pending);
|
copyFigures(s, pending);
|
||||||
|
pending.setConfirmationReceivedOn(
|
||||||
|
InvestmentPurchaseWritePlatformServiceImpl.confirmationReceivedOn(command, pending.getDealDate()));
|
||||||
|
pending.setConfirmationRef(
|
||||||
|
StringUtils.trimToNull(command.stringValueOfParameterNamedAllowingNull(InvestmentApiConstants.CONFIRMATION_REF)));
|
||||||
pending.setStatus(STATUS_SETTLED);
|
pending.setStatus(STATUS_SETTLED);
|
||||||
pending.setGlTransactionId(posted.getTransactionId());
|
pending.setGlTransactionId(posted.getTransactionId());
|
||||||
pending.setVoucherNo(voucherNo(posted));
|
pending.setVoucherNo(voucherNo(posted));
|
||||||
@@ -148,13 +163,18 @@ public class InvestmentSaleWritePlatformServiceImpl implements InvestmentSaleWri
|
|||||||
@Transactional
|
@Transactional
|
||||||
public CommandProcessingResult deleteSale(final Long saleId) {
|
public CommandProcessingResult deleteSale(final Long saleId) {
|
||||||
final InvestmentSale s = sale(saleId);
|
final InvestmentSale s = sale(saleId);
|
||||||
|
if (STATUS_DROPPED.equals(s.getStatus())) {
|
||||||
|
throw InvestmentSaleQuoter.rule("sale.dropped", "This sale is already dropped");
|
||||||
|
}
|
||||||
if (!STATUS_PENDING_SETTLEMENT.equals(s.getStatus())) {
|
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");
|
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();
|
final Long purchaseId = s.getPurchase().getId();
|
||||||
this.saleRepository.delete(s);
|
// Kept as DROPPED rather than deleted, so its sale number stays accounted for (shown as cancelled on the register).
|
||||||
this.saleRepository.flush();
|
s.setStatus(STATUS_DROPPED);
|
||||||
return new CommandProcessingResultBuilder().withEntityId(saleId).withSubEntityId(purchaseId).build();
|
this.saleRepository.saveAndFlush(s);
|
||||||
|
return new CommandProcessingResultBuilder().withEntityId(saleId).withSubEntityId(purchaseId).withResourceIdAsString(s.getSaleNo())
|
||||||
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -175,6 +195,7 @@ public class InvestmentSaleWritePlatformServiceImpl implements InvestmentSaleWri
|
|||||||
+ p.getScript().getScriptDesc(),
|
+ p.getScript().getScriptDesc(),
|
||||||
lines);
|
lines);
|
||||||
|
|
||||||
|
s.setSaleNo(InvestmentPurchaseWritePlatformServiceImpl.nextSaleNo(this.jdbcTemplate, s.getDealDate()));
|
||||||
s.setGlTransactionId(posted.getTransactionId());
|
s.setGlTransactionId(posted.getTransactionId());
|
||||||
s.setVoucherNo(voucherNo(posted));
|
s.setVoucherNo(voucherNo(posted));
|
||||||
s.setSettledOnDate(DateUtils.getBusinessLocalDate());
|
s.setSettledOnDate(DateUtils.getBusinessLocalDate());
|
||||||
|
|||||||
@@ -204,4 +204,5 @@
|
|||||||
<include file="parts/0610_investment_valuation_and_fund_switch.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0610_investment_valuation_and_fund_switch.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0611_investment_maker_checker.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0611_investment_maker_checker.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0612_investment_share.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0612_investment_share.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0613_investment_deal_slip.xml" relativeToChangelogFile="true"/>
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
<?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">
|
||||||
|
<!--
|
||||||
|
Deal slips: every deal is serially numbered and carries the confirmation mode to the counterparty, the broker of a
|
||||||
|
broker deal, and the counterparty's confirmation the back office records on settlement. A purchase already has its
|
||||||
|
deal number; a sale, redemption or switch now gets its own sale number (financial year, "S", running number, e.g.
|
||||||
|
202627S00001). Write-offs and amortisation are not deals and get none. Existing sales are numbered in the order
|
||||||
|
they were entered within the financial year of their deal date.
|
||||||
|
-->
|
||||||
|
<changeSet author="cbs-anshul" id="0613-1-investment-purchase-confirmation">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><columnExists tableName="m_investment_purchase" columnName="confirmation_mode"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<addColumn tableName="m_investment_purchase">
|
||||||
|
<column name="confirmation_mode" type="VARCHAR(10)"/>
|
||||||
|
<column name="confirmation_received_on" type="DATE"/>
|
||||||
|
<column name="confirmation_ref" type="VARCHAR(100)"/>
|
||||||
|
</addColumn>
|
||||||
|
</changeSet>
|
||||||
|
<changeSet author="cbs-anshul" id="0613-2-investment-sale-deal-slip">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><columnExists tableName="m_investment_sale" columnName="sale_no"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<addColumn tableName="m_investment_sale">
|
||||||
|
<column name="sale_no" type="VARCHAR(20)"/>
|
||||||
|
<column name="broker_code" type="VARCHAR(20)"/>
|
||||||
|
<column name="confirmation_mode" type="VARCHAR(10)"/>
|
||||||
|
<column name="confirmation_received_on" type="DATE"/>
|
||||||
|
<column name="confirmation_ref" type="VARCHAR(100)"/>
|
||||||
|
</addColumn>
|
||||||
|
</changeSet>
|
||||||
|
<changeSet author="cbs-anshul" id="0613-3-investment-sale-number-existing">
|
||||||
|
<sql>
|
||||||
|
UPDATE m_investment_sale s
|
||||||
|
SET sale_no = n.sale_no
|
||||||
|
FROM (SELECT b.id, b.fy || 'S' || LPAD(CAST(ROW_NUMBER() OVER (PARTITION BY b.fy ORDER BY b.id) AS VARCHAR), 5, '0') AS sale_no
|
||||||
|
FROM (SELECT a.id, CAST(a.fy_start AS VARCHAR) || LPAD(CAST(MOD(a.fy_start + 1, 100) AS VARCHAR), 2, '0') AS fy
|
||||||
|
FROM (SELECT id,
|
||||||
|
CASE WHEN EXTRACT(MONTH FROM deal_date) >= 4 THEN CAST(EXTRACT(YEAR FROM deal_date) AS INTEGER)
|
||||||
|
ELSE CAST(EXTRACT(YEAR FROM deal_date) AS INTEGER) - 1 END AS fy_start
|
||||||
|
FROM m_investment_sale
|
||||||
|
WHERE sale_type IN ('SELL', 'REDEEM', 'SWITCH') AND sale_no IS NULL) a) b) n
|
||||||
|
WHERE s.id = n.id
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
<changeSet author="cbs-anshul" id="0613-4-investment-sale-number-unique">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><indexExists tableName="m_investment_sale" indexName="uq_investment_sale_no"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<addUniqueConstraint tableName="m_investment_sale" columnNames="sale_no" constraintName="uq_investment_sale_no"/>
|
||||||
|
</changeSet>
|
||||||
|
</databaseChangeLog>
|
||||||
Reference in New Issue
Block a user