From 4ad508ee9678538fd2c67796cac3c448fb345f84 Mon Sep 17 00:00:00 2001 From: anshul Date: Mon, 12 Oct 2026 15:27:07 +0530 Subject: [PATCH] feat(investment): Daily Script Master backend Market rate of each script for a day (open / high / low / close), as the bank's existing CBS keeps it, so Purchase Entry can take its rate from here instead of the dealer typing it (Jitendra Sir). - m_investment_daily_rate, one row per script per day (migration 0596), with READ/CREATE/DELETE permissions granted to Investment menu roles. - /v1/investment/daily-rates: list (optionally per script), save, drop, and /latest for the newest rate of a script on or before a date. - Close rate is mandatory; high may not be below low, and open / close must sit inside low-high, with readable messages. Co-Authored-By: Claude Opus 5.5 --- .../service/CommandWrapperBuilder.java | 16 +++ .../api/InvestmentDailyRateApiConstants.java | 41 +++++++ .../api/InvestmentDailyRateApiResource.java | 96 ++++++++++++++++ .../data/InvestmentDailyRateData.java | 50 ++++++++ .../domain/InvestmentDailyRate.java | 62 ++++++++++ .../domain/InvestmentDailyRateRepository.java | 34 ++++++ .../InvestmentDailyRateNotFoundException.java | 28 +++++ ...eateInvestmentDailyRateCommandHandler.java | 42 +++++++ ...leteInvestmentDailyRateCommandHandler.java | 42 +++++++ ...ilyRateCommandFromApiJsonDeserializer.java | 103 +++++++++++++++++ ...nvestmentDailyRateReadPlatformService.java | 56 +++++++++ ...vestmentDailyRateWritePlatformService.java | 29 +++++ ...mentDailyRateWritePlatformServiceImpl.java | 87 ++++++++++++++ .../tenant/final-changelog-tenant.xml | 1 + .../parts/0596_investment_daily_rate.xml | 108 ++++++++++++++++++ 15 files changed, 795 insertions(+) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentDailyRateApiConstants.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentDailyRateApiResource.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/data/InvestmentDailyRateData.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/domain/InvestmentDailyRate.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/domain/InvestmentDailyRateRepository.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/exception/InvestmentDailyRateNotFoundException.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/handler/CreateInvestmentDailyRateCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/handler/DeleteInvestmentDailyRateCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/serialization/InvestmentDailyRateCommandFromApiJsonDeserializer.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentDailyRateReadPlatformService.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentDailyRateWritePlatformService.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentDailyRateWritePlatformServiceImpl.java create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0596_investment_daily_rate.xml diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index a51f7d09b..d4c0153be 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -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"; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentDailyRateApiConstants.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentDailyRateApiConstants.java new file mode 100644 index 000000000..8818becb6 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentDailyRateApiConstants.java @@ -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 PARAMS = Set.of(SCRIPT_ID, RATE_DATE, OPEN_RATE, HIGH_RATE, LOW_RATE, CLOSE_RATE, LOCALE, DATE_FORMAT); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentDailyRateApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentDailyRateApiResource.java new file mode 100644 index 000000000..9542c1664 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/api/InvestmentDailyRateApiResource.java @@ -0,0 +1,96 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.investment.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 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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/data/InvestmentDailyRateData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/data/InvestmentDailyRateData.java new file mode 100644 index 000000000..ae32661c6 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/data/InvestmentDailyRateData.java @@ -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(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/domain/InvestmentDailyRate.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/domain/InvestmentDailyRate.java new file mode 100644 index 000000000..46d1e0efa --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/domain/InvestmentDailyRate.java @@ -0,0 +1,62 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.investment.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 { + + @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; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/domain/InvestmentDailyRateRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/domain/InvestmentDailyRateRepository.java new file mode 100644 index 000000000..00d2fd29d --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/domain/InvestmentDailyRateRepository.java @@ -0,0 +1,34 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.investment.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 { + + boolean existsByScriptIdAndRateDate(Long scriptId, LocalDate rateDate); + + /** The newest rate of a script on or before a date. */ + Optional findFirstByScriptIdAndRateDateLessThanEqualOrderByRateDateDesc(Long scriptId, LocalDate onDate); + + List findByScriptIdOrderByRateDateDesc(Long scriptId); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/exception/InvestmentDailyRateNotFoundException.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/exception/InvestmentDailyRateNotFoundException.java new file mode 100644 index 000000000..b7deef16f --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/exception/InvestmentDailyRateNotFoundException.java @@ -0,0 +1,28 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.investment.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException; + +public class InvestmentDailyRateNotFoundException extends AbstractPlatformResourceNotFoundException { + + public InvestmentDailyRateNotFoundException(final Long id) { + super("error.msg.investmentdailyrate.id.invalid", "Daily script rate with identifier " + id + " does not exist", id); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/handler/CreateInvestmentDailyRateCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/handler/CreateInvestmentDailyRateCommandHandler.java new file mode 100644 index 000000000..ee633ffe1 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/handler/CreateInvestmentDailyRateCommandHandler.java @@ -0,0 +1,42 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.investment.handler; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.investment.service.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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/handler/DeleteInvestmentDailyRateCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/handler/DeleteInvestmentDailyRateCommandHandler.java new file mode 100644 index 000000000..f5cc29e12 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/handler/DeleteInvestmentDailyRateCommandHandler.java @@ -0,0 +1,42 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.investment.handler; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.investment.service.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()); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/serialization/InvestmentDailyRateCommandFromApiJsonDeserializer.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/serialization/InvestmentDailyRateCommandFromApiJsonDeserializer.java new file mode 100644 index 000000000..9e6bf0c74 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/serialization/InvestmentDailyRateCommandFromApiJsonDeserializer.java @@ -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>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, PARAMS); + final JsonElement element = this.fromApiJsonHelper.parse(json); + + final List errors = new ArrayList<>(); + final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(RESOURCE); + + 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; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentDailyRateReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentDailyRateReadPlatformService.java new file mode 100644 index 000000000..cfe996314 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentDailyRateReadPlatformService.java @@ -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 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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentDailyRateWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentDailyRateWritePlatformService.java new file mode 100644 index 000000000..73fe5f331 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentDailyRateWritePlatformService.java @@ -0,0 +1,29 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.investment.service; + +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; + +public interface InvestmentDailyRateWritePlatformService { + + CommandProcessingResult createDailyRate(JsonCommand command); + + CommandProcessingResult deleteDailyRate(Long rateId); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentDailyRateWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentDailyRateWritePlatformServiceImpl.java new file mode 100644 index 000000000..c7d6735cc --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/investment/service/InvestmentDailyRateWritePlatformServiceImpl.java @@ -0,0 +1,87 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.investment.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(); + } +} diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml index bde32cb03..bffc1aa86 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml @@ -170,4 +170,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0596_investment_daily_rate.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0596_investment_daily_rate.xml new file mode 100644 index 000000000..4f4dd49a9 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0596_investment_daily_rate.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SELECT COUNT(*) FROM m_permission WHERE code IN ('READ_INVESTMENTDAILYRATE', 'CREATE_INVESTMENTDAILYRATE', 'DELETE_INVESTMENTDAILYRATE', 'CREATE_INVESTMENTDAILYRATE_CHECKER', 'DELETE_INVESTMENTDAILYRATE_CHECKER') + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO m_role_permission (role_id, permission_id) + SELECT rp.role_id, p.id + FROM m_role_permission rp + JOIN m_permission menu ON menu.id = rp.permission_id AND menu.code = 'MENU_INVESTMENT' + CROSS JOIN m_permission p + WHERE p.code IN ('READ_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) + + +