Compare commits
3
Commits
41aeb2b89a
...
6380e0d7d2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6380e0d7d2 | ||
|
|
4ad508ee96 | ||
|
|
6eb562548c |
+16
@@ -151,6 +151,22 @@ public class CommandWrapperBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder createInvestmentDailyRate() {
|
||||
this.actionName = "CREATE";
|
||||
this.entityName = "INVESTMENTDAILYRATE";
|
||||
this.entityId = null;
|
||||
this.href = "/investment/daily-rates";
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder deleteInvestmentDailyRate(final Long rateId) {
|
||||
this.actionName = "DELETE";
|
||||
this.entityName = "INVESTMENTDAILYRATE";
|
||||
this.entityId = rateId;
|
||||
this.href = "/investment/daily-rates/" + rateId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder createChequeStockLot() {
|
||||
this.actionName = "CREATE";
|
||||
this.entityName = "CHEQUESTOCKLOT";
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.investment.api;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/** Constants for the Daily Script Master (market rate of a script for a day). */
|
||||
public final class InvestmentDailyRateApiConstants {
|
||||
|
||||
private InvestmentDailyRateApiConstants() {}
|
||||
|
||||
public static final String ENTITY = "INVESTMENTDAILYRATE";
|
||||
public static final String RESOURCE = "investmentdailyrate";
|
||||
|
||||
public static final String SCRIPT_ID = "scriptId";
|
||||
public static final String RATE_DATE = "rateDate";
|
||||
public static final String OPEN_RATE = "openRate";
|
||||
public static final String HIGH_RATE = "highRate";
|
||||
public static final String LOW_RATE = "lowRate";
|
||||
public static final String CLOSE_RATE = "closeRate";
|
||||
public static final String LOCALE = "locale";
|
||||
public static final String DATE_FORMAT = "dateFormat";
|
||||
|
||||
public static final Set<String> PARAMS = Set.of(SCRIPT_ID, RATE_DATE, OPEN_RATE, HIGH_RATE, LOW_RATE, CLOSE_RATE, LOCALE, DATE_FORMAT);
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.investment.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.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.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.investment.data.InvestmentDailyRateData;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentDailyRateReadPlatformService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Path("/v1/investment/daily-rates")
|
||||
@Component
|
||||
@Tag(name = "Investment Daily Script Master", description = "Market rate of each script for a day; Purchase Entry takes its rate from here.")
|
||||
@RequiredArgsConstructor
|
||||
public class InvestmentDailyRateApiResource {
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final InvestmentDailyRateReadPlatformService readPlatformService;
|
||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||
|
||||
@GET
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "List daily script rates, newest first")
|
||||
public List<InvestmentDailyRateData> retrieveAll(@QueryParam("scriptId") @Parameter(description = "scriptId") final Long scriptId) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(InvestmentDailyRateApiConstants.ENTITY);
|
||||
return this.readPlatformService.retrieveAll(scriptId);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("latest")
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Newest rate of a script on or before a date", description = "onDate (dd-MM-yyyy) defaults to the business "
|
||||
+ "date. Returns no content when no rate has been entered for the script.")
|
||||
public InvestmentDailyRateData retrieveLatest(@QueryParam("scriptId") @Parameter(description = "scriptId") final Long scriptId,
|
||||
@QueryParam("onDate") @Parameter(description = "dd-MM-yyyy") final String onDate) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(InvestmentDailyRateApiConstants.ENTITY);
|
||||
final LocalDate on = StringUtils.isBlank(onDate) ? DateUtils.getBusinessLocalDate()
|
||||
: LocalDate.parse(onDate.trim(), DateTimeFormatter.ofPattern("dd-MM-yyyy"));
|
||||
return this.readPlatformService.retrieveLatest(scriptId, on);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Save a daily script rate")
|
||||
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
final CommandWrapper request = new CommandWrapperBuilder().createInvestmentDailyRate().withJson(apiRequestBodyAsJson).build();
|
||||
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("{rateId}")
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Drop a daily script rate")
|
||||
public CommandProcessingResult delete(@PathParam("rateId") @Parameter(description = "rateId") final Long rateId) {
|
||||
final CommandWrapper request = new CommandWrapperBuilder().deleteInvestmentDailyRate(rateId).build();
|
||||
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -45,6 +45,7 @@ public final class InvestmentPurchaseApiConstants {
|
||||
public static final String NAV = "nav";
|
||||
public static final String STAMP_DUTY = "stampDuty";
|
||||
public static final String BROKERAGE = "brokerage";
|
||||
public static final String OTHER_CHARGES = "otherCharges";
|
||||
public static final String CATEGORY = "category";
|
||||
public static final String LAST_INT_DATE = "lastIntDate";
|
||||
public static final String CR_GL_ACCOUNT_ID = "crGlAccountId";
|
||||
@@ -55,6 +56,6 @@ public final class InvestmentPurchaseApiConstants {
|
||||
public static final String DATE_FORMAT = "dateFormat";
|
||||
|
||||
public static final Set<String> PARAMS = Set.of(BOND_TYPE, SCRIPT_ID, PARTY_ID, DEAL_DATE, DEAL_TIME, SETTLEMENT_TYPE, FACE_VALUE,
|
||||
PURCHASE_RATE, CONSIDER_AMT, NAV, STAMP_DUTY, BROKERAGE, CATEGORY, LAST_INT_DATE, CR_GL_ACCOUNT_ID, SUB_TYPE, OTHER_DETAILS,
|
||||
LOCALE, DATE_FORMAT);
|
||||
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);
|
||||
}
|
||||
|
||||
+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.data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentDailyRate;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
public class InvestmentDailyRateData {
|
||||
|
||||
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
||||
|
||||
private final Long id;
|
||||
private final Long scriptId;
|
||||
private final String scriptCode;
|
||||
private final String scriptDesc;
|
||||
private final String bondType;
|
||||
private final String rateDate;
|
||||
private final BigDecimal openRate;
|
||||
private final BigDecimal highRate;
|
||||
private final BigDecimal lowRate;
|
||||
private final BigDecimal closeRate;
|
||||
|
||||
public static InvestmentDailyRateData from(final InvestmentDailyRate r) {
|
||||
return InvestmentDailyRateData.builder().id(r.getId()).scriptId(r.getScript().getId()).scriptCode(r.getScript().getScriptCode())
|
||||
.scriptDesc(r.getScript().getScriptDesc()).bondType(r.getScript().getBondType())
|
||||
.rateDate(r.getRateDate().format(DATE_FORMAT)).openRate(r.getOpenRate()).highRate(r.getHighRate()).lowRate(r.getLowRate())
|
||||
.closeRate(r.getCloseRate()).build();
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -45,7 +45,9 @@ public class InvestmentInterestScheduleData {
|
||||
private final String dealDate;
|
||||
private final String lastIntDate;
|
||||
private final String maturityDate;
|
||||
/** Interest paid to the seller at purchase — already in Interest Receivable. */
|
||||
/**
|
||||
* Broken-period interest paid to the seller at purchase — booked as an expense (GL 500606), not in this schedule.
|
||||
*/
|
||||
private final BigDecimal purchasedAccruedInterest;
|
||||
private final BigDecimal couponPerHalfYear;
|
||||
private final String asOnDate;
|
||||
|
||||
+9
-8
@@ -52,6 +52,7 @@ public class InvestmentPurchaseData {
|
||||
private final BigDecimal balanceUnits;
|
||||
private final BigDecimal brokerage;
|
||||
private final BigDecimal stampDuty;
|
||||
private final BigDecimal otherCharges;
|
||||
private final BigDecimal bookValue;
|
||||
private final BigDecimal premiumDiscount;
|
||||
private final BigDecimal intRate;
|
||||
@@ -80,14 +81,14 @@ public class InvestmentPurchaseData {
|
||||
.partyName(p.getParty().getPartyName()).dealDate(format(p.getDealDate())).dealTime(p.getDealTime())
|
||||
.settlementType(p.getSettlementType()).settlementDate(format(p.getSettlementDate())).faceValue(p.getFaceValue())
|
||||
.purchaseRate(p.getPurchaseRate()).units(p.getUnits()).balanceUnits(p.getBalanceUnits()).brokerage(p.getBrokerage())
|
||||
.stampDuty(p.getStampDuty()).bookValue(p.getBookValue()).premiumDiscount(p.getPremiumDiscount()).intRate(p.getIntRate())
|
||||
.issueDate(format(p.getIssueDate())).maturityDate(format(p.getMaturityDate())).lastIntDate(format(p.getLastIntDate()))
|
||||
.accruedDays(p.getAccruedDays()).accruedInterest(p.getAccruedInterest()).settlementAmount(p.getSettlementAmount())
|
||||
.ytm(p.getYtm()).category(p.getCategory()).subType(p.getSubType())
|
||||
.investmentGlAccountId(p.getInvestmentGlAccount().getId()).investmentGlCode(p.getInvestmentGlAccount().getGlCode())
|
||||
.investmentGlName(p.getInvestmentGlAccount().getName()).crGlAccountId(p.getCrGlAccount().getId())
|
||||
.crGlCode(p.getCrGlAccount().getGlCode()).crGlName(p.getCrGlAccount().getName()).otherDetails(p.getOtherDetails())
|
||||
.status(p.getStatus()).build();
|
||||
.stampDuty(p.getStampDuty()).otherCharges(p.getOtherCharges()).bookValue(p.getBookValue())
|
||||
.premiumDiscount(p.getPremiumDiscount()).intRate(p.getIntRate()).issueDate(format(p.getIssueDate()))
|
||||
.maturityDate(format(p.getMaturityDate())).lastIntDate(format(p.getLastIntDate())).accruedDays(p.getAccruedDays())
|
||||
.accruedInterest(p.getAccruedInterest()).settlementAmount(p.getSettlementAmount()).ytm(p.getYtm()).category(p.getCategory())
|
||||
.subType(p.getSubType()).investmentGlAccountId(p.getInvestmentGlAccount().getId())
|
||||
.investmentGlCode(p.getInvestmentGlAccount().getGlCode()).investmentGlName(p.getInvestmentGlAccount().getName())
|
||||
.crGlAccountId(p.getCrGlAccount().getId()).crGlCode(p.getCrGlAccount().getGlCode()).crGlName(p.getCrGlAccount().getName())
|
||||
.otherDetails(p.getOtherDetails()).status(p.getStatus()).build();
|
||||
}
|
||||
|
||||
private static String format(final LocalDate date) {
|
||||
|
||||
+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.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.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
|
||||
/**
|
||||
* Daily Script Master: the market rate of a script for one day, as the bank's existing CBS keeps it. Purchase Entry
|
||||
* takes its rate from here (Jitendra Sir). Close rate is mandatory; open, high and low are optional.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "m_investment_daily_rate")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class InvestmentDailyRate extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "script_id", nullable = false)
|
||||
private InvestmentScript script;
|
||||
|
||||
@Column(name = "rate_date", nullable = false)
|
||||
private LocalDate rateDate;
|
||||
|
||||
@Column(name = "open_rate", scale = 6, precision = 19)
|
||||
private BigDecimal openRate;
|
||||
|
||||
@Column(name = "high_rate", scale = 6, precision = 19)
|
||||
private BigDecimal highRate;
|
||||
|
||||
@Column(name = "low_rate", scale = 6, precision = 19)
|
||||
private BigDecimal lowRate;
|
||||
|
||||
@Column(name = "close_rate", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal closeRate;
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.investment.domain;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface InvestmentDailyRateRepository extends JpaRepository<InvestmentDailyRate, Long> {
|
||||
|
||||
boolean existsByScriptIdAndRateDate(Long scriptId, LocalDate rateDate);
|
||||
|
||||
/** The newest rate of a script on or before a date. */
|
||||
Optional<InvestmentDailyRate> findFirstByScriptIdAndRateDateLessThanEqualOrderByRateDateDesc(Long scriptId, LocalDate onDate);
|
||||
|
||||
List<InvestmentDailyRate> findByScriptIdOrderByRateDateDesc(Long scriptId);
|
||||
}
|
||||
+5
-2
@@ -33,8 +33,8 @@ import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDa
|
||||
|
||||
/**
|
||||
* Investment Purchase Entry — one deal (as in the bank's existing CBS "Govt.Security/Bond Entry"). Once saved it is the
|
||||
* holding that interest, sale and redemption later work against; {@link #balanceUnits} tracks what is still held.
|
||||
* Every derived figure (units, book value, accrued interest, settlement amount) is computed by the backend.
|
||||
* holding that interest, sale and redemption later work against; {@link #balanceUnits} tracks what is still held. Every
|
||||
* derived figure (units, book value, accrued interest, settlement amount) is computed by the backend.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "m_investment_purchase")
|
||||
@@ -93,6 +93,9 @@ public class InvestmentPurchase extends AbstractAuditableWithUTCDateTimeCustom<L
|
||||
@Column(name = "stamp_duty", scale = 6, precision = 19)
|
||||
private BigDecimal stampDuty;
|
||||
|
||||
@Column(name = "other_charges", scale = 6, precision = 19)
|
||||
private BigDecimal otherCharges;
|
||||
|
||||
@Column(name = "book_value", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal bookValue;
|
||||
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.investment.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
|
||||
|
||||
public class InvestmentDailyRateNotFoundException extends AbstractPlatformResourceNotFoundException {
|
||||
|
||||
public InvestmentDailyRateNotFoundException(final Long id) {
|
||||
super("error.msg.investmentdailyrate.id.invalid", "Daily script rate with identifier " + id + " does not exist", id);
|
||||
}
|
||||
}
|
||||
+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.InvestmentDailyRateWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "INVESTMENTDAILYRATE", action = "CREATE")
|
||||
@RequiredArgsConstructor
|
||||
public class CreateInvestmentDailyRateCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final InvestmentDailyRateWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.createDailyRate(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.InvestmentDailyRateWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "INVESTMENTDAILYRATE", action = "DELETE")
|
||||
@RequiredArgsConstructor
|
||||
public class DeleteInvestmentDailyRateCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final InvestmentDailyRateWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.deleteDailyRate(command.entityId());
|
||||
}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.investment.serialization;
|
||||
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentDailyRateApiConstants.CLOSE_RATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentDailyRateApiConstants.HIGH_RATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentDailyRateApiConstants.LOW_RATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentDailyRateApiConstants.OPEN_RATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentDailyRateApiConstants.PARAMS;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentDailyRateApiConstants.RATE_DATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentDailyRateApiConstants.RESOURCE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentDailyRateApiConstants.SCRIPT_ID;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import java.lang.reflect.Type;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/** Validates a Daily Script Master entry: close rate is mandatory and the day's rates must sit inside low to high. */
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public final class InvestmentDailyRateCommandFromApiJsonDeserializer {
|
||||
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
|
||||
public void validateForCreate(final String json) {
|
||||
if (StringUtils.isBlank(json)) {
|
||||
throw new InvalidJsonException();
|
||||
}
|
||||
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
||||
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, PARAMS);
|
||||
final JsonElement element = this.fromApiJsonHelper.parse(json);
|
||||
|
||||
final List<ApiParameterError> errors = new ArrayList<>();
|
||||
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(RESOURCE);
|
||||
|
||||
v.reset().parameter(SCRIPT_ID).value(this.fromApiJsonHelper.extractLongNamed(SCRIPT_ID, element)).notNull().longGreaterThanZero();
|
||||
v.reset().parameter(RATE_DATE).value(this.fromApiJsonHelper.extractLocalDateNamed(RATE_DATE, element)).notNull();
|
||||
final BigDecimal close = amount(CLOSE_RATE, element);
|
||||
final BigDecimal open = amount(OPEN_RATE, element);
|
||||
final BigDecimal high = amount(HIGH_RATE, element);
|
||||
final BigDecimal low = amount(LOW_RATE, element);
|
||||
v.reset().parameter(CLOSE_RATE).value(close).notNull().positiveAmount();
|
||||
v.reset().parameter(OPEN_RATE).value(open).ignoreIfNull().positiveAmount();
|
||||
v.reset().parameter(HIGH_RATE).value(high).ignoreIfNull().positiveAmount();
|
||||
v.reset().parameter(LOW_RATE).value(low).ignoreIfNull().positiveAmount();
|
||||
|
||||
if (high != null && low != null && high.compareTo(low) < 0) {
|
||||
v.reset().parameter(HIGH_RATE).failWithCodeAndMessage("less.than.low.rate", "High Rate cannot be below Low Rate");
|
||||
}
|
||||
outsideRange(v, CLOSE_RATE, "Close Rate", close, low, high);
|
||||
outsideRange(v, OPEN_RATE, "Open Rate", open, low, high);
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors);
|
||||
}
|
||||
}
|
||||
|
||||
private static void outsideRange(final DataValidatorBuilder v, final String param, final String label, final BigDecimal rate,
|
||||
final BigDecimal low, final BigDecimal high) {
|
||||
if (rate == null) {
|
||||
return;
|
||||
}
|
||||
if (low != null && rate.compareTo(low) < 0) {
|
||||
v.reset().parameter(param).failWithCodeAndMessage("below.low.rate", label + " cannot be below Low Rate");
|
||||
} else if (high != null && rate.compareTo(high) > 0) {
|
||||
v.reset().parameter(param).failWithCodeAndMessage("above.high.rate", label + " cannot be above High Rate");
|
||||
}
|
||||
}
|
||||
|
||||
private BigDecimal amount(final String param, final JsonElement element) {
|
||||
return this.fromApiJsonHelper.parameterExists(param, element)
|
||||
? this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(param, element)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
+2
@@ -29,6 +29,7 @@ import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApi
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.FACE_VALUE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.LAST_INT_DATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.NAV;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.OTHER_CHARGES;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.OTHER_DETAILS;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.PARAMS;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.PARTY_ID;
|
||||
@@ -110,6 +111,7 @@ public final class InvestmentPurchaseCommandFromApiJsonDeserializer {
|
||||
}
|
||||
v.reset().parameter(BROKERAGE).value(amount(BROKERAGE, element)).ignoreIfNull().zeroOrPositiveAmount();
|
||||
v.reset().parameter(STAMP_DUTY).value(amount(STAMP_DUTY, element)).ignoreIfNull().zeroOrPositiveAmount();
|
||||
v.reset().parameter(OTHER_CHARGES).value(amount(OTHER_CHARGES, element)).ignoreIfNull().zeroOrPositiveAmount();
|
||||
|
||||
final String dealTime = this.fromApiJsonHelper.extractStringNamed(DEAL_TIME, element);
|
||||
if (StringUtils.isNotBlank(dealTime)) {
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* 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.time.LocalDate;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.portfolio.investment.data.InvestmentDailyRateData;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentDailyRateRepository;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional(readOnly = true)
|
||||
public class InvestmentDailyRateReadPlatformService {
|
||||
|
||||
/** The register shows the latest rates first; older ones are reached by picking a script. */
|
||||
private static final int MAX_ROWS = 500;
|
||||
|
||||
private final InvestmentDailyRateRepository dailyRateRepository;
|
||||
|
||||
public List<InvestmentDailyRateData> retrieveAll(final Long scriptId) {
|
||||
if (scriptId != null) {
|
||||
return this.dailyRateRepository.findByScriptIdOrderByRateDateDesc(scriptId).stream().map(InvestmentDailyRateData::from)
|
||||
.toList();
|
||||
}
|
||||
return this.dailyRateRepository
|
||||
.findAll(PageRequest.of(0, MAX_ROWS, Sort.by(Sort.Direction.DESC, "rateDate").and(Sort.by(Sort.Direction.DESC, "id"))))
|
||||
.stream().map(InvestmentDailyRateData::from).toList();
|
||||
}
|
||||
|
||||
/** The newest rate of a script on or before {@code onDate}; null when none has been entered. */
|
||||
public InvestmentDailyRateData retrieveLatest(final Long scriptId, final LocalDate onDate) {
|
||||
return this.dailyRateRepository.findFirstByScriptIdAndRateDateLessThanEqualOrderByRateDateDesc(scriptId, onDate)
|
||||
.map(InvestmentDailyRateData::from).orElse(null);
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.investment.service;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
|
||||
public interface InvestmentDailyRateWritePlatformService {
|
||||
|
||||
CommandProcessingResult createDailyRate(JsonCommand command);
|
||||
|
||||
CommandProcessingResult deleteDailyRate(Long rateId);
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.investment.service;
|
||||
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentDailyRateApiConstants.CLOSE_RATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentDailyRateApiConstants.HIGH_RATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentDailyRateApiConstants.LOW_RATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentDailyRateApiConstants.OPEN_RATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentDailyRateApiConstants.RATE_DATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentDailyRateApiConstants.SCRIPT_ID;
|
||||
|
||||
import java.time.LocalDate;
|
||||
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.exception.GeneralPlatformDomainRuleException;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentDailyRate;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentDailyRateRepository;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentScript;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentScriptRepository;
|
||||
import org.apache.fineract.portfolio.investment.exception.InvestmentDailyRateNotFoundException;
|
||||
import org.apache.fineract.portfolio.investment.exception.InvestmentScriptNotFoundException;
|
||||
import org.apache.fineract.portfolio.investment.serialization.InvestmentDailyRateCommandFromApiJsonDeserializer;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/** Saves and drops Daily Script Master rates. A wrong rate is corrected by dropping it and entering it again. */
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class InvestmentDailyRateWritePlatformServiceImpl implements InvestmentDailyRateWritePlatformService {
|
||||
|
||||
private final InvestmentDailyRateCommandFromApiJsonDeserializer validator;
|
||||
private final InvestmentDailyRateRepository dailyRateRepository;
|
||||
private final InvestmentScriptRepository scriptRepository;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public CommandProcessingResult createDailyRate(final JsonCommand command) {
|
||||
this.validator.validateForCreate(command.json());
|
||||
final Long scriptId = command.longValueOfParameterNamed(SCRIPT_ID);
|
||||
final InvestmentScript script = this.scriptRepository.findById(scriptId)
|
||||
.orElseThrow(() -> new InvestmentScriptNotFoundException(scriptId));
|
||||
final LocalDate rateDate = command.localDateValueOfParameterNamed(RATE_DATE);
|
||||
if (this.dailyRateRepository.existsByScriptIdAndRateDate(scriptId, rateDate)) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.investmentdailyrate.duplicate",
|
||||
"A rate for " + script.getScriptCode() + " on this date already exists. Drop it first to correct it.");
|
||||
}
|
||||
|
||||
final InvestmentDailyRate rate = new InvestmentDailyRate();
|
||||
rate.setScript(script);
|
||||
rate.setRateDate(rateDate);
|
||||
rate.setOpenRate(command.bigDecimalValueOfParameterNamed(OPEN_RATE));
|
||||
rate.setHighRate(command.bigDecimalValueOfParameterNamed(HIGH_RATE));
|
||||
rate.setLowRate(command.bigDecimalValueOfParameterNamed(LOW_RATE));
|
||||
rate.setCloseRate(command.bigDecimalValueOfParameterNamed(CLOSE_RATE));
|
||||
final InvestmentDailyRate saved = this.dailyRateRepository.saveAndFlush(rate);
|
||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(saved.getId())
|
||||
.withResourceIdAsString(script.getScriptCode()).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public CommandProcessingResult deleteDailyRate(final Long rateId) {
|
||||
final InvestmentDailyRate rate = this.dailyRateRepository.findById(rateId)
|
||||
.orElseThrow(() -> new InvestmentDailyRateNotFoundException(rateId));
|
||||
this.dailyRateRepository.delete(rate);
|
||||
this.dailyRateRepository.flush();
|
||||
return new CommandProcessingResultBuilder().withEntityId(rateId).build();
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -35,6 +35,8 @@ import java.util.TreeSet;
|
||||
* Interest Receivable / Cr Interest Income), even though the cash arrives at the next coupon;</li>
|
||||
* <li>when the coupon arrives, the provision already booked is cleared and only the rest is new income.</li>
|
||||
* </ul>
|
||||
* Broken-period interest paid to the seller at purchase is an expense of its own (RBI UCB Investment Directions, para
|
||||
* 152; GL 500606), so it is not carried here: the first coupon is income in full.
|
||||
*/
|
||||
public final class InvestmentInterestCalculator {
|
||||
|
||||
@@ -129,11 +131,9 @@ public final class InvestmentInterestCalculator {
|
||||
* coupon date immediately before the purchase
|
||||
* @param dealDate
|
||||
* purchase deal date — closings before it belong to the seller
|
||||
* @param purchasedAccrued
|
||||
* interest paid to the seller at purchase; it already sits in Interest Receivable
|
||||
*/
|
||||
public static List<ScheduleRow> schedule(final BigDecimal faceValue, final BigDecimal ratePercent, final LocalDate maturity,
|
||||
final LocalDate lastIntDate, final LocalDate dealDate, final BigDecimal purchasedAccrued) {
|
||||
final LocalDate lastIntDate, final LocalDate dealDate) {
|
||||
final List<ScheduleRow> rows = new ArrayList<>();
|
||||
if (faceValue == null || ratePercent == null || ratePercent.signum() <= 0 || maturity == null || lastIntDate == null
|
||||
|| dealDate == null || !maturity.isAfter(lastIntDate)) {
|
||||
@@ -148,7 +148,7 @@ public final class InvestmentInterestCalculator {
|
||||
}
|
||||
|
||||
final BigDecimal coupon = halfYearCoupon(faceValue, ratePercent);
|
||||
BigDecimal receivable = purchasedAccrued == null ? BigDecimal.ZERO : purchasedAccrued;
|
||||
BigDecimal receivable = BigDecimal.ZERO;
|
||||
LocalDate previousCoupon = lastIntDate;
|
||||
for (final LocalDate date : events) {
|
||||
final int days = days360(previousCoupon, date);
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ public class InvestmentPurchaseReadPlatformService {
|
||||
final BigDecimal rate = p.getIntRate() == null ? BigDecimal.ZERO : p.getIntRate();
|
||||
|
||||
final List<InvestmentInterestScheduleData.Row> rows = InvestmentInterestCalculator
|
||||
.schedule(faceHeld, rate, p.getMaturityDate(), p.getLastIntDate(), p.getDealDate(), p.getAccruedInterest()).stream()
|
||||
.schedule(faceHeld, rate, p.getMaturityDate(), p.getLastIntDate(), p.getDealDate()).stream()
|
||||
.map(r -> 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()).amount(r.amount()).due(!r.date().isAfter(asOn)).build())
|
||||
|
||||
+18
-14
@@ -28,6 +28,7 @@ import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApi
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.FACE_VALUE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.LAST_INT_DATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.NAV;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.OTHER_CHARGES;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.OTHER_DETAILS;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.PARTY_ID;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.PURCHASE_RATE;
|
||||
@@ -131,13 +132,17 @@ public class InvestmentPurchaseWritePlatformServiceImpl implements InvestmentPur
|
||||
p.setOtherDetails(otherDetails(command));
|
||||
p.setStatus(STATUS_ACTIVE);
|
||||
|
||||
final BigDecimal brokerage = zeroIfNull(command.bigDecimalValueOfParameterNamed(BROKERAGE));
|
||||
p.setBrokerage(brokerage);
|
||||
// Brokerage, stamp duty and other charges are expenses of the deal (Jitendra Sir): paid with the
|
||||
// settlement, but kept out of the book value so the charges stay visible on their own GLs.
|
||||
p.setBrokerage(zeroIfNull(command.bigDecimalValueOfParameterNamed(BROKERAGE)));
|
||||
p.setStampDuty(zeroIfNull(command.bigDecimalValueOfParameterNamed(STAMP_DUTY)));
|
||||
p.setOtherCharges(zeroIfNull(command.bigDecimalValueOfParameterNamed(OTHER_CHARGES)));
|
||||
if ("MUTUAL_FUND".equals(bondType)) {
|
||||
computeMutualFund(p, command, brokerage);
|
||||
computeMutualFund(p, command);
|
||||
} else {
|
||||
computeSecurity(p, command, script, brokerage);
|
||||
computeSecurity(p, command, script);
|
||||
}
|
||||
p.setSettlementAmount(p.getBookValue().add(p.getAccruedInterest()).add(totalCharges(p)));
|
||||
|
||||
final InvestmentPurchase saved = this.purchaseRepository.saveAndFlush(p);
|
||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(saved.getId())
|
||||
@@ -158,8 +163,7 @@ public class InvestmentPurchaseWritePlatformServiceImpl implements InvestmentPur
|
||||
}
|
||||
|
||||
/** Govt Security / Bond: face value at a price per ₹100, plus interest owed to the seller since the last coupon. */
|
||||
private void computeSecurity(final InvestmentPurchase p, final JsonCommand command, final InvestmentScript script,
|
||||
final BigDecimal brokerage) {
|
||||
private void computeSecurity(final InvestmentPurchase p, final JsonCommand command, final InvestmentScript script) {
|
||||
final BigDecimal faceValue = command.bigDecimalValueOfParameterNamed(FACE_VALUE);
|
||||
final BigDecimal rate = command.bigDecimalValueOfParameterNamed(PURCHASE_RATE);
|
||||
final LocalDate lastIntDate = command.localDateValueOfParameterNamed(LAST_INT_DATE);
|
||||
@@ -177,31 +181,31 @@ public class InvestmentPurchaseWritePlatformServiceImpl implements InvestmentPur
|
||||
p.setPurchaseRate(rate);
|
||||
p.setUnits(units);
|
||||
p.setBalanceUnits(units);
|
||||
p.setBookValue(cost.add(brokerage));
|
||||
p.setBookValue(cost);
|
||||
p.setPremiumDiscount(faceValue.multiply(rate.subtract(HUNDRED)).divide(HUNDRED, 2, RoundingMode.HALF_UP));
|
||||
p.setLastIntDate(lastIntDate);
|
||||
p.setAccruedDays(days);
|
||||
p.setAccruedInterest(accrued);
|
||||
p.setSettlementAmount(p.getBookValue().add(accrued));
|
||||
p.setYtm(approxYtm(intRate, rate, script.getIssueDate(), script.getMaturityDate()));
|
||||
}
|
||||
|
||||
/** Mutual fund: the amount invested buys units at the NAV; stamp duty and brokerage add to the cost. */
|
||||
private void computeMutualFund(final InvestmentPurchase p, final JsonCommand command, final BigDecimal brokerage) {
|
||||
/** Mutual fund: the amount invested buys units at the NAV. */
|
||||
private void computeMutualFund(final InvestmentPurchase p, final JsonCommand command) {
|
||||
final BigDecimal amount = command.bigDecimalValueOfParameterNamed(CONSIDER_AMT);
|
||||
final BigDecimal nav = command.bigDecimalValueOfParameterNamed(NAV);
|
||||
final BigDecimal stampDuty = zeroIfNull(command.bigDecimalValueOfParameterNamed(STAMP_DUTY));
|
||||
final BigDecimal units = amount.divide(nav, 3, RoundingMode.HALF_UP);
|
||||
|
||||
p.setFaceValue(amount);
|
||||
p.setPurchaseRate(nav);
|
||||
p.setUnits(units);
|
||||
p.setBalanceUnits(units);
|
||||
p.setStampDuty(stampDuty);
|
||||
p.setBookValue(amount.add(stampDuty).add(brokerage));
|
||||
p.setBookValue(amount);
|
||||
p.setAccruedInterest(BigDecimal.ZERO);
|
||||
p.setAccruedDays(0);
|
||||
p.setSettlementAmount(p.getBookValue());
|
||||
}
|
||||
|
||||
private static BigDecimal totalCharges(final InvestmentPurchase p) {
|
||||
return p.getBrokerage().add(p.getStampDuty()).add(p.getOtherCharges());
|
||||
}
|
||||
|
||||
/** Approximate yield: coupon adjusted for the discount/premium spread over the security's life, over the price. */
|
||||
|
||||
@@ -169,4 +169,6 @@
|
||||
<include file="parts/0593_tds_interest_receipts_and_deductions.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0593_investment_masters.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0594_investment_purchase.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0595_investment_purchase_other_charges.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0596_investment_daily_rate.xml" relativeToChangelogFile="true"/>
|
||||
</databaseChangeLog>
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<?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">
|
||||
|
||||
<!--
|
||||
Other charges on an investment deal (legal, SGL, CCIL, ...), kept next to brokerage and stamp
|
||||
duty. All three are expenses of the deal (Jitendra Sir): they are paid with the settlement but
|
||||
are not part of the book value of the investment.
|
||||
-->
|
||||
|
||||
<changeSet author="cbs-anshul" id="0595-1-add-investment-purchase-other-charges">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not><columnExists tableName="m_investment_purchase" columnName="other_charges"/></not>
|
||||
</preConditions>
|
||||
<addColumn tableName="m_investment_purchase">
|
||||
<column name="other_charges" type="DECIMAL(19,6)"/>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
</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">
|
||||
|
||||
<!--
|
||||
Daily Script Master: the market rate of each script for a day (open / high / low / close),
|
||||
entered by treasury from FBIL / CCIL. Purchase Entry takes its rate from here instead of the
|
||||
dealer typing it (Jitendra Sir). One row per script per day.
|
||||
-->
|
||||
|
||||
<changeSet author="cbs-anshul" id="0596-1-create-investment-daily-rate">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not><tableExists tableName="m_investment_daily_rate"/></not>
|
||||
</preConditions>
|
||||
<createTable tableName="m_investment_daily_rate">
|
||||
<column name="id" type="BIGINT" autoIncrement="true"><constraints nullable="false" primaryKey="true"/></column>
|
||||
<column name="script_id" type="BIGINT"><constraints nullable="false"/></column>
|
||||
<column name="rate_date" type="DATE"><constraints nullable="false"/></column>
|
||||
<column name="open_rate" type="DECIMAL(19,6)"/>
|
||||
<column name="high_rate" type="DECIMAL(19,6)"/>
|
||||
<column name="low_rate" type="DECIMAL(19,6)"/>
|
||||
<column name="close_rate" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
|
||||
<column name="created_by" type="BIGINT"><constraints nullable="false"/></column>
|
||||
<column name="created_on_utc" type="DATETIME"><constraints nullable="false"/></column>
|
||||
<column name="last_modified_by" type="BIGINT"><constraints nullable="false"/></column>
|
||||
<column name="last_modified_on_utc" type="DATETIME"><constraints nullable="false"/></column>
|
||||
</createTable>
|
||||
<addUniqueConstraint tableName="m_investment_daily_rate" columnNames="script_id, rate_date"
|
||||
constraintName="uq_investment_daily_rate_script_date"/>
|
||||
<addForeignKeyConstraint baseTableName="m_investment_daily_rate" baseColumnNames="script_id" constraintName="fk_investment_daily_rate_script"
|
||||
referencedTableName="m_investment_script" referencedColumnNames="id"/>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="cbs-anshul" id="0596-2-investment-daily-rate-permissions">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code IN ('READ_INVESTMENTDAILYRATE', 'CREATE_INVESTMENTDAILYRATE', 'DELETE_INVESTMENTDAILYRATE', 'CREATE_INVESTMENTDAILYRATE_CHECKER', 'DELETE_INVESTMENTDAILYRATE_CHECKER')</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="portfolio"/>
|
||||
<column name="code" value="READ_INVESTMENTDAILYRATE"/>
|
||||
<column name="entity_name" value="INVESTMENTDAILYRATE"/>
|
||||
<column name="action_name" value="READ"/>
|
||||
<column name="can_maker_checker" valueBoolean="false"/>
|
||||
</insert>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="portfolio"/>
|
||||
<column name="code" value="CREATE_INVESTMENTDAILYRATE"/>
|
||||
<column name="entity_name" value="INVESTMENTDAILYRATE"/>
|
||||
<column name="action_name" value="CREATE"/>
|
||||
<column name="can_maker_checker" valueBoolean="false"/>
|
||||
</insert>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="portfolio"/>
|
||||
<column name="code" value="DELETE_INVESTMENTDAILYRATE"/>
|
||||
<column name="entity_name" value="INVESTMENTDAILYRATE"/>
|
||||
<column name="action_name" value="DELETE"/>
|
||||
<column name="can_maker_checker" valueBoolean="false"/>
|
||||
</insert>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="portfolio"/>
|
||||
<column name="code" value="CREATE_INVESTMENTDAILYRATE_CHECKER"/>
|
||||
<column name="entity_name" value="INVESTMENTDAILYRATE"/>
|
||||
<column name="action_name" value="CREATE_CHECKER"/>
|
||||
<column name="can_maker_checker" valueBoolean="false"/>
|
||||
</insert>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="portfolio"/>
|
||||
<column name="code" value="DELETE_INVESTMENTDAILYRATE_CHECKER"/>
|
||||
<column name="entity_name" value="INVESTMENTDAILYRATE"/>
|
||||
<column name="action_name" value="DELETE_CHECKER"/>
|
||||
<column name="can_maker_checker" valueBoolean="false"/>
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
<!-- Same roles as the other investment screens: whoever sees the Investment menu. -->
|
||||
<changeSet author="cbs-anshul" id="0596-3-grant-investment-daily-rate-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_INVESTMENTDAILYRATE', 'CREATE_INVESTMENTDAILYRATE', 'DELETE_INVESTMENTDAILYRATE')
|
||||
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>
|
||||
+11
-11
@@ -100,15 +100,15 @@ class InvestmentInterestCalculatorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void scheduleClearsPurchasedInterestAndProvisionsAtEachClosing() {
|
||||
final List<ScheduleRow> rows = InvestmentInterestCalculator.schedule(ONE_CRORE, RATE, MATURITY, LAST_COUPON, DEAL,
|
||||
new BigDecimal("120833"));
|
||||
void scheduleProvisionsAtEachClosingAndClearsAtTheCoupon() {
|
||||
final List<ScheduleRow> rows = InvestmentInterestCalculator.schedule(ONE_CRORE, RATE, MATURITY, LAST_COUPON, DEAL);
|
||||
|
||||
// 16 coupons + 15 closings (Mar 2027 … Mar 2034); 30-09-2026 is before the deal, so it is the seller's.
|
||||
assertEquals(31, rows.size());
|
||||
|
||||
// First coupon: 1,20,833 paid to the seller is recovered, only the rest is our income.
|
||||
assertRow(rows.get(0), TYPE_COUPON, LocalDate.of(2027, 2, 10), LocalDate.of(2026, 8, 11), 180, "375000", "120833", "254167");
|
||||
// First coupon is income in full: the 1,20,833 paid to the seller was booked as an expense at purchase (RBI
|
||||
// para 152).
|
||||
assertRow(rows.get(0), TYPE_COUPON, LocalDate.of(2027, 2, 10), LocalDate.of(2026, 8, 11), 180, "375000", "0", "375000");
|
||||
// 31 March closing: 50 days earned since 10 Feb, nothing booked yet.
|
||||
assertRow(rows.get(1), TYPE_PROVISION, LocalDate.of(2027, 3, 31), LocalDate.of(2027, 2, 11), 50, "104167", "0", "104167");
|
||||
// August coupon clears the March provision.
|
||||
@@ -119,25 +119,25 @@ class InvestmentInterestCalculatorTest {
|
||||
assertEquals(TYPE_COUPON, last.type());
|
||||
assertEquals(MATURITY, last.date());
|
||||
|
||||
// Over the life, income booked = all coupons − interest bought from the seller.
|
||||
// Over the life, income booked = all the coupons.
|
||||
final BigDecimal totalIncome = rows.stream().map(ScheduleRow::amount).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
assertEquals(new BigDecimal("375000").multiply(BigDecimal.valueOf(16)).subtract(new BigDecimal("120833")), totalIncome);
|
||||
assertEquals(new BigDecimal("375000").multiply(BigDecimal.valueOf(16)), totalIncome);
|
||||
}
|
||||
|
||||
@Test
|
||||
void closingOnACouponDateIsNotProvisionedTwice() {
|
||||
// Coupons on 31 Mar / 30 Sep: every closing is a coupon day, so there are no provision lines.
|
||||
final List<ScheduleRow> rows = InvestmentInterestCalculator.schedule(ONE_CRORE, RATE, LocalDate.of(2028, 3, 31),
|
||||
LocalDate.of(2026, 9, 30), DEAL, BigDecimal.ZERO);
|
||||
LocalDate.of(2026, 9, 30), DEAL);
|
||||
assertEquals(3, rows.size());
|
||||
assertTrue(rows.stream().allMatch(r -> TYPE_COUPON.equals(r.type())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void scheduleIsEmptyWithoutRateOrDates() {
|
||||
assertTrue(InvestmentInterestCalculator.schedule(ONE_CRORE, BigDecimal.ZERO, MATURITY, LAST_COUPON, DEAL, null).isEmpty());
|
||||
assertTrue(InvestmentInterestCalculator.schedule(ONE_CRORE, RATE, null, LAST_COUPON, DEAL, null).isEmpty());
|
||||
assertTrue(InvestmentInterestCalculator.schedule(ONE_CRORE, RATE, MATURITY, null, DEAL, null).isEmpty());
|
||||
assertTrue(InvestmentInterestCalculator.schedule(ONE_CRORE, BigDecimal.ZERO, MATURITY, LAST_COUPON, DEAL).isEmpty());
|
||||
assertTrue(InvestmentInterestCalculator.schedule(ONE_CRORE, RATE, null, LAST_COUPON, DEAL).isEmpty());
|
||||
assertTrue(InvestmentInterestCalculator.schedule(ONE_CRORE, RATE, MATURITY, null, DEAL).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user