diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandSourceService.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandSourceService.java index 75840388d..ea8a7294d 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandSourceService.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandSourceService.java @@ -137,7 +137,7 @@ public class CommandSourceService { String permission = commandSource.getPermissionCode(); boolean isMakerChecker = configurationDomainService.isMakerCheckerEnabledForTask(permission); if (isMakerChecker || result.isRollbackTransaction()) { - if (isApprovedByChecker || user.isCheckerSuperUser()) { + if (isApprovedByChecker || isSuperUserBypassAllowed(commandSource, user)) { commandSource.markAsChecked(user); } else { if (commandSource.isSanitized()) { @@ -151,6 +151,22 @@ public class CommandSourceService { return result; } + private boolean isSuperUserBypassAllowed(CommandSource commandSource, AppUser user) { + if (!user.isCheckerSuperUser()) { + return false; + } + if ("GLACCOUNT".equalsIgnoreCase(commandSource.getEntityName()) + || "CBSBASEINTERESTRATE".equalsIgnoreCase(commandSource.getEntityName()) + || "CHARGE".equalsIgnoreCase(commandSource.getEntityName()) + || "LOANPRODUCT".equalsIgnoreCase(commandSource.getEntityName()) + || "SAVINGSPRODUCT".equalsIgnoreCase(commandSource.getEntityName()) + || "CURRENTPRODUCT".equalsIgnoreCase(commandSource.getEntityName()) + || "SHAREPRODUCT".equalsIgnoreCase(commandSource.getEntityName())) { + return false; + } + return true; + } + private void sanitizeJson(@NonNull CommandSource commandSource, Set sanitizeKeys) { if (sanitizeKeys == null || sanitizeKeys.isEmpty()) { return; 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 5e2c31402..44288c368 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 @@ -2211,6 +2211,14 @@ public class CommandWrapperBuilder { return this; } + public CommandWrapperBuilder createCbsBaseInterestRate() { + this.actionName = "CREATE"; + this.entityName = "CBSBASEINTERESTRATE"; + this.entityId = null; + this.href = "/fixeddepositproducts/baseinterestrates"; + return this; + } + public CommandWrapperBuilder updateFixedDepositProduct(final Long productId) { this.actionName = "UPDATE"; this.entityName = "FIXEDDEPOSITPRODUCT"; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositProductsBaseInterestRatesApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositProductsBaseInterestRatesApiResource.java index f740409c4..fd5badcf3 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositProductsBaseInterestRatesApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositProductsBaseInterestRatesApiResource.java @@ -29,6 +29,10 @@ import jakarta.ws.rs.core.MediaType; import java.time.LocalDate; import java.util.List; import lombok.RequiredArgsConstructor; +import org.apache.fineract.commands.domain.CommandWrapper; +import org.apache.fineract.commands.service.CommandWrapperBuilder; +import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.portfolio.savings.DepositsApiConstants; import org.apache.fineract.portfolio.savings.data.FdInterestRateEffectiveData; @@ -42,6 +46,7 @@ public class FixedDepositProductsBaseInterestRatesApiResource { private final PlatformSecurityContext context; private final FdBaseInterestRateReadPlatformService fdBaseInterestRateReadPlatformService; + private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; private final Gson gson = new GsonBuilder() .registerTypeAdapter(LocalDate.class, (JsonSerializer) (src, typeOfSrc, ctx) -> new JsonPrimitive(src.toString())) @@ -77,11 +82,11 @@ public class FixedDepositProductsBaseInterestRatesApiResource { @Produces({ MediaType.APPLICATION_JSON }) @Operation(summary = "Save new FD base interest rate version", description = "Creates a new FD CBS base chart version for the given fdType and deactivates the previous active version.") @ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") }) - public String saveInterestRates(@Parameter(hidden = true) final String apiRequestBodyAsJson) { - this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.FIXED_DEPOSIT_PRODUCT_RESOURCE_NAME); - final FdInterestRateSaveRequest request = gson.fromJson(apiRequestBodyAsJson, FdInterestRateSaveRequest.class); - final Long createdById = this.context.authenticatedUser().getId(); - final FdInterestRateEffectiveData saved = this.fdBaseInterestRateReadPlatformService.saveInterestRates(request, createdById); - return gson.toJson(saved); + public CommandProcessingResult saveInterestRates(@Parameter(hidden = true) final String apiRequestBodyAsJson) { + final CommandWrapper commandRequest = new CommandWrapperBuilder() + .createCbsBaseInterestRate() + .withJson(apiRequestBodyAsJson) + .build(); + return this.commandsSourceWritePlatformService.logCommandSource(commandRequest); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/CreateCbsBaseInterestRateCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/CreateCbsBaseInterestRateCommandHandler.java new file mode 100644 index 000000000..3d4673fb5 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/CreateCbsBaseInterestRateCommandHandler.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.savings.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.savings.service.FdBaseInterestRateWritePlatformService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@CommandType(entity = "CBSBASEINTERESTRATE", action = "CREATE") +@RequiredArgsConstructor +public class CreateCbsBaseInterestRateCommandHandler implements NewCommandSourceHandler { + + private final FdBaseInterestRateWritePlatformService writePlatformService; + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + return this.writePlatformService.saveInterestRates(command); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/FdBaseInterestRateReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/FdBaseInterestRateReadPlatformServiceImpl.java index 516eb97c8..c7b0acd63 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/FdBaseInterestRateReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/FdBaseInterestRateReadPlatformServiceImpl.java @@ -11,10 +11,16 @@ */ package org.apache.fineract.portfolio.savings.service; +import java.math.BigDecimal; import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.data.ApiParameterError; +import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.portfolio.savings.api.FdInterestRateSaveRequest; import org.apache.fineract.portfolio.savings.data.FdInterestRateEffectiveData; import org.apache.fineract.portfolio.savings.data.FdInterestRateSlabData; @@ -51,7 +57,9 @@ public class FdBaseInterestRateReadPlatformServiceImpl implements FdBaseInterest @Override @Transactional public FdInterestRateEffectiveData saveInterestRates(FdInterestRateSaveRequest request, Long createdById) { - final String fdType = request.getFdType(); + validateRequest(request); + + final String fdType = request.getFdType().toUpperCase(); final LocalDate today = LocalDate.now(DateUtils.getDateTimeZoneOfTenant()); final List activeVersions = effectiveRepository @@ -85,6 +93,125 @@ public class FdBaseInterestRateReadPlatformServiceImpl implements FdBaseInterest return toData(newVersion); } + private void validateRequest(final FdInterestRateSaveRequest request) { + final List errors = new ArrayList<>(); + final DataValidatorBuilder validator = new DataValidatorBuilder(errors).resource("FD_BASE_INTEREST_RATE_VERSION"); + + if (request == null) { + validator.failWithCodeNoParameterAddedToErrorCode("request.cannot.be.null"); + throwExceptionIfValidationWarningsExist(errors); + return; + } + + final String fdType = request.getFdType(); + if (fdType == null || fdType.isBlank()) { + validator.parameter("fdType").failWithCode("cannot.be.blank"); + } else if (!List.of("SINGLE", "DOUBLE", "SPECIAL").contains(fdType.toUpperCase())) { + validator.parameter("fdType").value(fdType).failWithCode("invalid.fd.type"); + } + + if (request.getSlabs() == null || request.getSlabs().isEmpty()) { + validator.parameter("slabs").failWithCode("cannot.be.blank"); + } + + if (request.getVersionLabel() != null && request.getVersionLabel().length() > 100) { + validator.parameter("versionLabel").value(request.getVersionLabel()).failWithCode("exceeds.max.length"); + } + + throwExceptionIfValidationWarningsExist(errors); + validateSlabs(request.getSlabs()); + } + + private void validateSlabs(final List slabs) { + final List errors = new ArrayList<>(); + final DataValidatorBuilder validator = new DataValidatorBuilder(errors).resource("FD_BASE_INTEREST_RATE_VERSION"); + + if (slabs == null || slabs.isEmpty()) { + validator.parameter("slabs").failWithCode("cannot.be.blank"); + throwExceptionIfValidationWarningsExist(errors); + return; + } + + final List sorted = new ArrayList<>(slabs); + sorted.sort(Comparator.comparing((FdInterestRateSaveRequest.SlabRequest slab) -> slab.getSrNo() == null ? Integer.MAX_VALUE : slab.getSrNo()) + .thenComparing(slab -> slab.getFromPeriod() == null ? Integer.MAX_VALUE : slab.getFromPeriod())); + + final Integer expectedPeriodType = sorted.get(0).getPeriodTypeEnum(); + Integer expectedFrom = 1; + + for (int i = 0; i < sorted.size(); i++) { + final FdInterestRateSaveRequest.SlabRequest slab = sorted.get(i); + final int srNo = slab.getSrNo() == null ? i + 1 : slab.getSrNo(); + final String slabPrefix = "slabs[" + srNo + "]."; + + if (slab.getPeriodTypeEnum() == null) { + validator.parameter(slabPrefix + "periodTypeEnum").failWithCode("cannot.be.blank"); + } else if (!slab.getPeriodTypeEnum().equals(expectedPeriodType)) { + validator.parameter(slabPrefix + "periodTypeEnum").value(slab.getPeriodTypeEnum()) + .failWithCode("must.be.consistent.across.all.slabs"); + } + + final Integer fromPeriod = slab.getFromPeriod(); + if (fromPeriod == null) { + validator.parameter(slabPrefix + "fromPeriod").failWithCode("cannot.be.blank"); + } else if (fromPeriod < 0) { + validator.parameter(slabPrefix + "fromPeriod").value(fromPeriod).failWithCode("must.be.non.negative"); + } else if (!fromPeriod.equals(expectedFrom)) { + if (i == 0) { + validator.parameter(slabPrefix + "fromPeriod").value(fromPeriod).failWithCode("first.slab.must.start.at.one"); + } else if (fromPeriod < expectedFrom) { + validator.parameter(slabPrefix + "fromPeriod").value(fromPeriod).failWithCode("must.not.overlap.previous.range"); + } else { + validator.parameter(slabPrefix + "fromPeriod").value(fromPeriod).failWithCode("must.be.continuous.with.previous.range"); + } + } + + final Integer toPeriod = slab.getToPeriod(); + if (i < sorted.size() - 1) { + if (toPeriod == null) { + validator.parameter(slabPrefix + "toPeriod").failWithCode("cannot.be.blank.for.intermediate.slab"); + } else { + if (toPeriod < 0) { + validator.parameter(slabPrefix + "toPeriod").value(toPeriod).failWithCode("must.be.non.negative"); + } + if (fromPeriod != null && toPeriod < fromPeriod) { + validator.parameter(slabPrefix + "toPeriod").value(toPeriod).failWithCode("cannot.be.less.than.fromPeriod"); + } + expectedFrom = toPeriod + 1; + } + } else { + if (toPeriod != null) { + if (toPeriod < 0) { + validator.parameter(slabPrefix + "toPeriod").value(toPeriod).failWithCode("must.be.non.negative"); + } + if (fromPeriod != null && toPeriod < fromPeriod) { + validator.parameter(slabPrefix + "toPeriod").value(toPeriod).failWithCode("cannot.be.less.than.fromPeriod"); + } + } + } + + final BigDecimal annualRate = slab.getAnnualInterestRate(); + if (annualRate == null) { + validator.parameter(slabPrefix + "annualInterestRate").failWithCode("cannot.be.blank"); + } else if (annualRate.compareTo(BigDecimal.ZERO) < 0) { + validator.parameter(slabPrefix + "annualInterestRate").value(annualRate).failWithCode("must.be.non.negative"); + } + + final BigDecimal seniorRate = slab.getSeniorCitizenRate(); + if (seniorRate != null && seniorRate.compareTo(BigDecimal.ZERO) < 0) { + validator.parameter(slabPrefix + "seniorCitizenRate").value(seniorRate).failWithCode("must.be.non.negative"); + } + } + + throwExceptionIfValidationWarningsExist(errors); + } + + private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + private FdInterestRateEffectiveData toData(FdBaseInterestRateEffective e) { List slabs = slabRepository.findByEffectiveIdOrderBySrNo(e.getId()).stream() .map(s -> FdInterestRateSlabData.builder() diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/FdBaseInterestRateWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/FdBaseInterestRateWritePlatformService.java new file mode 100644 index 000000000..100138c83 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/FdBaseInterestRateWritePlatformService.java @@ -0,0 +1,27 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.savings.service; + +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; + +public interface FdBaseInterestRateWritePlatformService { + + CommandProcessingResult saveInterestRates(JsonCommand command); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/FdBaseInterestRateWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/FdBaseInterestRateWritePlatformServiceImpl.java new file mode 100644 index 000000000..eb239c191 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/FdBaseInterestRateWritePlatformServiceImpl.java @@ -0,0 +1,49 @@ +/** + * 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.savings.service; + +import com.google.gson.Gson; +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.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.savings.api.FdInterestRateSaveRequest; +import org.apache.fineract.portfolio.savings.data.FdInterestRateEffectiveData; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class FdBaseInterestRateWritePlatformServiceImpl implements FdBaseInterestRateWritePlatformService { + + private final PlatformSecurityContext context; + private final FdBaseInterestRateReadPlatformService readPlatformService; + private final Gson gson = new Gson(); + + @Override + public CommandProcessingResult saveInterestRates(final JsonCommand command) { + final FdInterestRateSaveRequest request = gson.fromJson(command.json(), FdInterestRateSaveRequest.class); + final Long createdById = this.context.authenticatedUser().getId(); + final FdInterestRateEffectiveData saved = this.readPlatformService.saveInterestRates(request, createdById); + return new CommandProcessingResultBuilder() + .withCommandId(command.commandId()) + .withEntityId(saved.getId()) + .build(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java index 722d25070..e1aad5f58 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java @@ -169,6 +169,8 @@ import org.apache.fineract.portfolio.savings.service.RdBaseInterestRateReadPlatf import org.apache.fineract.portfolio.savings.service.RdBaseInterestRateReadPlatformServiceImpl; import org.apache.fineract.portfolio.savings.service.FdBaseInterestRateReadPlatformService; import org.apache.fineract.portfolio.savings.service.FdBaseInterestRateReadPlatformServiceImpl; +import org.apache.fineract.portfolio.savings.service.FdBaseInterestRateWritePlatformService; +import org.apache.fineract.portfolio.savings.service.FdBaseInterestRateWritePlatformServiceImpl; import org.apache.fineract.portfolio.savings.service.SavingsSchedularInterestPoster; import org.apache.fineract.portfolio.savings.service.SavingsSchedularInterestPosterTask; import org.apache.fineract.portfolio.savings.service.search.SavingsAccountTransactionSearchService; @@ -200,6 +202,14 @@ public class SavingsConfiguration { return new FdBaseInterestRateReadPlatformServiceImpl(effectiveRepository, slabRepository); } + @Bean + @ConditionalOnMissingBean(FdBaseInterestRateWritePlatformService.class) + public FdBaseInterestRateWritePlatformService fdBaseInterestRateWritePlatformService( + PlatformSecurityContext context, + FdBaseInterestRateReadPlatformService readPlatformService) { + return new FdBaseInterestRateWritePlatformServiceImpl(context, readPlatformService); + } + @Bean @ConditionalOnMissingBean(SavingsAccountTransactionSearchService.class) public SavingsAccountTransactionSearchService savingsAccountTransactionSearchService(PlatformSecurityContext context, diff --git a/fineract-provider/src/main/java/org/apache/fineract/useradministration/api/AppUserPreferencesApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/useradministration/api/AppUserPreferencesApiResource.java new file mode 100644 index 000000000..0ecd065ad --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/useradministration/api/AppUserPreferencesApiResource.java @@ -0,0 +1,79 @@ +package org.apache.fineract.useradministration.api; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.security.exception.NoAuthorizationException; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.useradministration.data.AppUserPreferenceData; +import org.apache.fineract.useradministration.service.AppUserPreferenceReadPlatformService; +import org.apache.fineract.useradministration.service.AppUserPreferenceWritePlatformService; +import org.springframework.stereotype.Component; + +@Path("/v1/users/{userId}/preferences") +@Component +@Tag(name = "User Preferences", description = "API to manage user specific preferences.") +@RequiredArgsConstructor +public class AppUserPreferencesApiResource { + + private final PlatformSecurityContext context; + private final AppUserPreferenceReadPlatformService readPlatformService; + private final AppUserPreferenceWritePlatformService writePlatformService; + + private void validateUserAuthorization(Long userId) { + if (!context.authenticatedUser().getId().equals(userId)) { + throw new NoAuthorizationException("Not authorized to access or modify preferences for another user."); + } + } + + @GET + @Path("{preferenceName}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve user preference", description = "Retrieves a specific preference for the user.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = AppUserPreferenceData.class))) }) + public AppUserPreferenceData getPreference(@PathParam("userId") @Parameter(description = "userId") final Long userId, + @PathParam("preferenceName") @Parameter(description = "preferenceName") final String preferenceName) { + + validateUserAuthorization(userId); + String value = readPlatformService.getPreferenceValue(userId, preferenceName); + return new AppUserPreferenceData(preferenceName, value); + } + + @PUT + @Path("{preferenceName}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Update user preference", description = "Updates or creates a specific preference for the user.") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = AppUserPreferenceData.class))) + @ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") }) + public AppUserPreferenceData updatePreference(@PathParam("userId") @Parameter(description = "userId") final Long userId, + @PathParam("preferenceName") @Parameter(description = "preferenceName") final String preferenceName, + final AppUserPreferenceData preferenceData) { + + validateUserAuthorization(userId); + + // Ensure the path parameter matches the request body if the request body contains a name + String valueToSave = preferenceData != null ? preferenceData.getValue() : null; + if (valueToSave == null) { + throw new IllegalArgumentException("Preference value cannot be null"); + } + + writePlatformService.updatePreference(userId, preferenceName, valueToSave); + return new AppUserPreferenceData(preferenceName, valueToSave); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/useradministration/data/AppUserPreferenceData.java b/fineract-provider/src/main/java/org/apache/fineract/useradministration/data/AppUserPreferenceData.java new file mode 100644 index 000000000..bcb37e359 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/useradministration/data/AppUserPreferenceData.java @@ -0,0 +1,13 @@ +package org.apache.fineract.useradministration.data; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AppUserPreferenceData { + private String name; + private String value; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/AppUserPreference.java b/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/AppUserPreference.java new file mode 100644 index 000000000..3ff371990 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/AppUserPreference.java @@ -0,0 +1,54 @@ +package org.apache.fineract.useradministration.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; + +@Entity +@Table(name = "m_appuser_preference") +public class AppUserPreference extends AbstractPersistableCustom { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "appuser_id", nullable = false) + private AppUser appUser; + + @Column(name = "name", length = 100, nullable = false) + private String name; + + @Column(name = "value", nullable = false) + private String value; + + protected AppUserPreference() { + // + } + + private AppUserPreference(final AppUser appUser, final String name, final String value) { + this.appUser = appUser; + this.name = name; + this.value = value; + } + + public static AppUserPreference create(final AppUser appUser, final String name, final String value) { + return new AppUserPreference(appUser, name, value); + } + + public void updateValue(final String newValue) { + this.value = newValue; + } + + public String getName() { + return this.name; + } + + public String getValue() { + return this.value; + } + + public AppUser getAppUser() { + return this.appUser; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/AppUserPreferenceRepository.java b/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/AppUserPreferenceRepository.java new file mode 100644 index 000000000..52279af43 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/AppUserPreferenceRepository.java @@ -0,0 +1,10 @@ +package org.apache.fineract.useradministration.domain; + +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AppUserPreferenceRepository extends JpaRepository { + + Optional findByAppUserIdAndName(Long appUserId, String name); + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/useradministration/service/AppUserPreferenceReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/useradministration/service/AppUserPreferenceReadPlatformService.java new file mode 100644 index 000000000..0973b1c4f --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/useradministration/service/AppUserPreferenceReadPlatformService.java @@ -0,0 +1,5 @@ +package org.apache.fineract.useradministration.service; + +public interface AppUserPreferenceReadPlatformService { + String getPreferenceValue(Long appUserId, String name); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/useradministration/service/AppUserPreferenceReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/useradministration/service/AppUserPreferenceReadPlatformServiceImpl.java new file mode 100644 index 000000000..bb2c46d54 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/useradministration/service/AppUserPreferenceReadPlatformServiceImpl.java @@ -0,0 +1,20 @@ +package org.apache.fineract.useradministration.service; + +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.useradministration.domain.AppUserPreference; +import org.apache.fineract.useradministration.domain.AppUserPreferenceRepository; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class AppUserPreferenceReadPlatformServiceImpl implements AppUserPreferenceReadPlatformService { + + private final AppUserPreferenceRepository appUserPreferenceRepository; + + @Override + public String getPreferenceValue(Long appUserId, String name) { + Optional pref = appUserPreferenceRepository.findByAppUserIdAndName(appUserId, name); + return pref.map(AppUserPreference::getValue).orElse(null); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/useradministration/service/AppUserPreferenceWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/useradministration/service/AppUserPreferenceWritePlatformService.java new file mode 100644 index 000000000..d32f3ed46 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/useradministration/service/AppUserPreferenceWritePlatformService.java @@ -0,0 +1,5 @@ +package org.apache.fineract.useradministration.service; + +public interface AppUserPreferenceWritePlatformService { + void updatePreference(Long appUserId, String name, String value); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/useradministration/service/AppUserPreferenceWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/useradministration/service/AppUserPreferenceWritePlatformServiceImpl.java new file mode 100644 index 000000000..2ca6ff41e --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/useradministration/service/AppUserPreferenceWritePlatformServiceImpl.java @@ -0,0 +1,32 @@ +package org.apache.fineract.useradministration.service; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.useradministration.domain.AppUser; +import org.apache.fineract.useradministration.domain.AppUserPreference; +import org.apache.fineract.useradministration.domain.AppUserPreferenceRepository; +import org.apache.fineract.useradministration.domain.AppUserRepository; +import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class AppUserPreferenceWritePlatformServiceImpl implements AppUserPreferenceWritePlatformService { + + private final AppUserPreferenceRepository appUserPreferenceRepository; + private final AppUserRepository appUserRepository; + + @Transactional + @Override + public void updatePreference(Long appUserId, String name, String value) { + AppUserPreference preference = appUserPreferenceRepository.findByAppUserIdAndName(appUserId, name).orElse(null); + if (preference == null) { + AppUser appUser = appUserRepository.findById(appUserId) + .orElseThrow(() -> new PlatformDataIntegrityException("error.msg.user.id.invalid", "User not found")); + preference = AppUserPreference.create(appUser, name, value); + } else { + preference.updateValue(value); + } + appUserPreferenceRepository.saveAndFlush(preference); + } +} diff --git a/fineract-provider/src/main/resources/application.properties b/fineract-provider/src/main/resources/application.properties index e39a6cdd6..3e57e55b5 100644 --- a/fineract-provider/src/main/resources/application.properties +++ b/fineract-provider/src/main/resources/application.properties @@ -46,7 +46,7 @@ fineract.security.oauth2.client.registrations.frontend-client.require-authorizat fineract.tenant.host=${FINERACT_DEFAULT_TENANTDB_HOSTNAME:localhost} fineract.tenant.port=${FINERACT_DEFAULT_TENANTDB_PORT:5432} fineract.tenant.username=${FINERACT_DEFAULT_TENANTDB_UID:postgres} -fineract.tenant.password=${FINERACT_DEFAULT_TENANTDB_PWD:313863} +fineract.tenant.password=${FINERACT_DEFAULT_TENANTDB_PWD:anshul@9009} fineract.tenant.parameters=${FINERACT_DEFAULT_TENANTDB_CONN_PARAMS:} fineract.tenant.timezone=${FINERACT_DEFAULT_TENANTDB_TIMEZONE:Asia/Kolkata} fineract.tenant.identifier=${FINERACT_DEFAULT_TENANTDB_IDENTIFIER:default} @@ -407,7 +407,7 @@ server.tomcat.mbeanregistry.enabled=${FINERACT_SERVER_TOMCAT_MBEANREGISTRY_ENABL spring.datasource.hikari.driverClassName=${FINERACT_HIKARI_DRIVER_SOURCE_CLASS_NAME:org.postgresql.Driver} spring.datasource.hikari.jdbcUrl=${FINERACT_HIKARI_JDBC_URL:jdbc:postgresql://localhost:5432/fineract_tenants} spring.datasource.hikari.username=${FINERACT_HIKARI_USERNAME:postgres} -spring.datasource.hikari.password=${FINERACT_HIKARI_PASSWORD:313863} +spring.datasource.hikari.password=${FINERACT_HIKARI_PASSWORD:anshul@9009} spring.datasource.hikari.minimumIdle=${FINERACT_HIKARI_MINIMUM_IDLE:3} spring.datasource.hikari.maximumPoolSize=${FINERACT_HIKARI_MAXIMUM_POOL_SIZE:10} spring.datasource.hikari.idleTimeout=${FINERACT_HIKARI_IDLE_TIMEOUT:60000} diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index d2407861e..8fd705c9f 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -537,4 +537,10 @@ + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0466_create_m_appuser_preference.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0466_create_m_appuser_preference.xml new file mode 100644 index 000000000..f2065b1b2 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0466_create_m_appuser_preference.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0467_enable_glaccount_maker_checker.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0467_enable_glaccount_maker_checker.xml new file mode 100644 index 000000000..9fee7092f --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0467_enable_glaccount_maker_checker.xml @@ -0,0 +1,23 @@ + + + + + ANY + + + SELECT COUNT(*) + FROM m_permission + WHERE entity_name = 'GLACCOUNT' + AND action_name IN ('CREATE', 'UPDATE', 'DELETE') + + + + UPDATE m_permission + SET can_maker_checker = TRUE + WHERE entity_name = 'GLACCOUNT' + AND action_name IN ('CREATE', 'UPDATE', 'DELETE'); + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0468_add_address_id_to_welcome_kit_dispatch.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0468_add_address_id_to_welcome_kit_dispatch.xml index d440dcb0d..852fbddff 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0468_add_address_id_to_welcome_kit_dispatch.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0468_add_address_id_to_welcome_kit_dispatch.xml @@ -7,6 +7,7 @@ address as verified or not verified once the outcome is known. Deliberately a plain column rather than a foreign key: the dispatch is a historical record and must survive the address being edited or removed. --> + ANY diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0469_create_database_export_request.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0469_create_database_export_request.xml index b373481da..e6892667a 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0469_create_database_export_request.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0469_create_database_export_request.xml @@ -7,6 +7,7 @@ person's authority. The requester states a reason, an admin approves or rejects, and only an approved request can produce a dump. Every step is stamped so the trail survives the file itself. --> + ANY @@ -50,6 +51,7 @@ + ANY INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) SELECT 'authorisation', 'CREATE_DATABASEEXPORT', 'DATABASEEXPORT', 'CREATE', false diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0470_enable_cbs_base_interest_rate_maker_checker.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0470_enable_cbs_base_interest_rate_maker_checker.xml new file mode 100644 index 000000000..c0e10637c --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0470_enable_cbs_base_interest_rate_maker_checker.xml @@ -0,0 +1,68 @@ + + + + + ANY + + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'portfolio', 'CREATE_CBSBASEINTERESTRATE', 'CBSBASEINTERESTRATE', 'CREATE', true + WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_CBSBASEINTERESTRATE'); + + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'portfolio', 'CREATE_CBSBASEINTERESTRATE_CHECKER', 'CBSBASEINTERESTRATE', 'CREATE_CHECKER', false + WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_CBSBASEINTERESTRATE_CHECKER'); + + UPDATE m_permission + SET can_maker_checker = TRUE + WHERE entity_name = 'CBSBASEINTERESTRATE' + AND action_name = 'CREATE'; + + UPDATE m_permission + SET action_name = 'CREATE_CHECKER' + WHERE entity_name = 'CBSBASEINTERESTRATE' + AND code = 'CREATE_CBSBASEINTERESTRATE_CHECKER'; + + + + + ANY + + INSERT INTO m_role_permission (role_id, permission_id) + SELECT r.id, p.id + FROM m_role r + CROSS JOIN m_permission p + WHERE r.name = 'Super user' + AND p.code IN ('CREATE_CBSBASEINTERESTRATE', 'CREATE_CBSBASEINTERESTRATE_CHECKER') + AND NOT EXISTS ( + SELECT 1 FROM m_role_permission rp + WHERE rp.role_id = r.id AND rp.permission_id = p.id + ); + + INSERT INTO m_role_permission (role_id, permission_id) + SELECT ur.role_id, p.id + FROM m_appuser u + JOIN m_appuser_role ur ON ur.appuser_id = u.id + CROSS JOIN m_permission p + WHERE u.username = 'bankadmin' + AND p.code IN ('CREATE_CBSBASEINTERESTRATE', 'CREATE_CBSBASEINTERESTRATE_CHECKER') + AND NOT EXISTS ( + SELECT 1 FROM m_role_permission rp + WHERE rp.role_id = ur.role_id AND rp.permission_id = p.id + ); + + INSERT INTO m_role_permission (role_id, permission_id) + SELECT r.id, p.id + FROM m_role r + CROSS JOIN m_permission p + WHERE LOWER(r.name) = 'clerk' + AND p.code IN ('CREATE_CBSBASEINTERESTRATE', 'CREATE_CBSBASEINTERESTRATE_CHECKER') + AND NOT EXISTS ( + SELECT 1 FROM m_role_permission rp + WHERE rp.role_id = r.id AND rp.permission_id = p.id + ); + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0471_enable_charge_maker_checker.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0471_enable_charge_maker_checker.xml new file mode 100644 index 000000000..f0f7dd2db --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0471_enable_charge_maker_checker.xml @@ -0,0 +1,63 @@ + + + + + ANY + + + SELECT COUNT(*) + FROM m_permission + WHERE entity_name = 'CHARGE' + AND action_name = 'CREATE' + + + + UPDATE m_permission + SET can_maker_checker = TRUE + WHERE entity_name = 'CHARGE' + AND action_name = 'CREATE'; + + + + + ANY + + INSERT INTO m_role_permission (role_id, permission_id) + SELECT r.id, p.id + FROM m_role r + CROSS JOIN m_permission p + WHERE r.name = 'Super user' + AND p.code IN ('CREATE_CHARGE', 'CREATE_CHARGE_CHECKER') + AND NOT EXISTS ( + SELECT 1 FROM m_role_permission rp + WHERE rp.role_id = r.id AND rp.permission_id = p.id + ); + + INSERT INTO m_role_permission (role_id, permission_id) + SELECT ur.role_id, p.id + FROM m_appuser u + JOIN m_appuser_role ur ON ur.appuser_id = u.id + CROSS JOIN m_permission p + WHERE u.username = 'bankadmin' + AND p.code IN ('CREATE_CHARGE', 'CREATE_CHARGE_CHECKER') + AND NOT EXISTS ( + SELECT 1 FROM m_role_permission rp + WHERE rp.role_id = ur.role_id AND rp.permission_id = p.id + ); + + INSERT INTO m_role_permission (role_id, permission_id) + SELECT r.id, p.id + FROM m_role r + CROSS JOIN m_permission p + WHERE LOWER(r.name) = 'clerk' + AND p.code IN ('CREATE_CHARGE', 'CREATE_CHARGE_CHECKER') + AND NOT EXISTS ( + SELECT 1 FROM m_role_permission rp + WHERE rp.role_id = r.id AND rp.permission_id = p.id + ); + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0472_enable_product_creation_maker_checker.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0472_enable_product_creation_maker_checker.xml new file mode 100644 index 000000000..ed4387a11 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0472_enable_product_creation_maker_checker.xml @@ -0,0 +1,87 @@ + + + + + ANY + + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'SHAREPRODUCT', 'CREATE_SHAREPRODUCT_CHECKER', 'SHAREPRODUCT', 'CREATE_CHECKER', FALSE + WHERE NOT EXISTS ( + SELECT 1 FROM m_permission WHERE code = 'CREATE_SHAREPRODUCT_CHECKER' + ); + + + + + ANY + + + SELECT COUNT(*) + FROM m_permission + WHERE code IN ('CREATE_LOANPRODUCT', 'CREATE_SAVINGSPRODUCT', 'CREATE_CURRENTPRODUCT', 'CREATE_SHAREPRODUCT') + + + + UPDATE m_permission + SET can_maker_checker = TRUE + WHERE code IN ('CREATE_LOANPRODUCT', 'CREATE_SAVINGSPRODUCT', 'CREATE_CURRENTPRODUCT', 'CREATE_SHAREPRODUCT'); + + + + + ANY + + INSERT INTO m_role_permission (role_id, permission_id) + SELECT r.id, p.id + FROM m_role r + CROSS JOIN m_permission p + WHERE r.name = 'Super user' + AND p.code IN ( + 'CREATE_LOANPRODUCT', 'CREATE_LOANPRODUCT_CHECKER', + 'CREATE_SAVINGSPRODUCT', 'CREATE_SAVINGSPRODUCT_CHECKER', + 'CREATE_CURRENTPRODUCT', 'CREATE_CURRENTPRODUCT_CHECKER', + 'CREATE_SHAREPRODUCT', 'CREATE_SHAREPRODUCT_CHECKER' + ) + AND NOT EXISTS ( + SELECT 1 FROM m_role_permission rp + WHERE rp.role_id = r.id AND rp.permission_id = p.id + ); + + INSERT INTO m_role_permission (role_id, permission_id) + SELECT ur.role_id, p.id + FROM m_appuser u + JOIN m_appuser_role ur ON ur.appuser_id = u.id + CROSS JOIN m_permission p + WHERE u.username = 'bankadmin' + AND p.code IN ( + 'CREATE_LOANPRODUCT', 'CREATE_LOANPRODUCT_CHECKER', + 'CREATE_SAVINGSPRODUCT', 'CREATE_SAVINGSPRODUCT_CHECKER', + 'CREATE_CURRENTPRODUCT', 'CREATE_CURRENTPRODUCT_CHECKER', + 'CREATE_SHAREPRODUCT', 'CREATE_SHAREPRODUCT_CHECKER' + ) + AND NOT EXISTS ( + SELECT 1 FROM m_role_permission rp + WHERE rp.role_id = ur.role_id AND rp.permission_id = p.id + ); + + INSERT INTO m_role_permission (role_id, permission_id) + SELECT r.id, p.id + FROM m_role r + CROSS JOIN m_permission p + WHERE LOWER(r.name) = 'clerk' + AND p.code IN ( + 'CREATE_LOANPRODUCT', 'CREATE_LOANPRODUCT_CHECKER', + 'CREATE_SAVINGSPRODUCT', 'CREATE_SAVINGSPRODUCT_CHECKER', + 'CREATE_CURRENTPRODUCT', 'CREATE_CURRENTPRODUCT_CHECKER', + 'CREATE_SHAREPRODUCT', 'CREATE_SHAREPRODUCT_CHECKER' + ) + AND NOT EXISTS ( + SELECT 1 FROM m_role_permission rp + WHERE rp.role_id = r.id AND rp.permission_id = p.id + ); + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0474_enable_fixed_deposit_product_maker_checker.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0474_enable_fixed_deposit_product_maker_checker.xml new file mode 100644 index 000000000..6321c262c --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0474_enable_fixed_deposit_product_maker_checker.xml @@ -0,0 +1,72 @@ + + + + + ANY + + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'portfolio', 'CREATE_FIXEDDEPOSITPRODUCT_CHECKER', 'FIXEDDEPOSITPRODUCT', 'CREATE_CHECKER', FALSE + WHERE NOT EXISTS ( + SELECT 1 FROM m_permission WHERE code = 'CREATE_FIXEDDEPOSITPRODUCT_CHECKER' + ); + + + + + ANY + + + SELECT COUNT(*) + FROM m_permission + WHERE code = 'CREATE_FIXEDDEPOSITPRODUCT' + + + + UPDATE m_permission + SET can_maker_checker = TRUE + WHERE code = 'CREATE_FIXEDDEPOSITPRODUCT'; + + + + + ANY + + INSERT INTO m_role_permission (role_id, permission_id) + SELECT r.id, p.id + FROM m_role r + CROSS JOIN m_permission p + WHERE r.name = 'Super user' + AND p.code IN ('CREATE_FIXEDDEPOSITPRODUCT', 'CREATE_FIXEDDEPOSITPRODUCT_CHECKER') + AND NOT EXISTS ( + SELECT 1 FROM m_role_permission rp + WHERE rp.role_id = r.id AND rp.permission_id = p.id + ); + + INSERT INTO m_role_permission (role_id, permission_id) + SELECT ur.role_id, p.id + FROM m_appuser u + JOIN m_appuser_role ur ON ur.appuser_id = u.id + CROSS JOIN m_permission p + WHERE u.username = 'bankadmin' + AND p.code IN ('CREATE_FIXEDDEPOSITPRODUCT', 'CREATE_FIXEDDEPOSITPRODUCT_CHECKER') + AND NOT EXISTS ( + SELECT 1 FROM m_role_permission rp + WHERE rp.role_id = ur.role_id AND rp.permission_id = p.id + ); + + INSERT INTO m_role_permission (role_id, permission_id) + SELECT r.id, p.id + FROM m_role r + CROSS JOIN m_permission p + WHERE LOWER(r.name) = 'clerk' + AND p.code IN ('CREATE_FIXEDDEPOSITPRODUCT', 'CREATE_FIXEDDEPOSITPRODUCT_CHECKER') + AND NOT EXISTS ( + SELECT 1 FROM m_role_permission rp + WHERE rp.role_id = r.id AND rp.permission_id = p.id + ); + + + + diff --git a/fineract-provider/src/test/java/org/apache/fineract/commands/service/CommandSourceServiceTest.java b/fineract-provider/src/test/java/org/apache/fineract/commands/service/CommandSourceServiceTest.java index 31c90c9e9..2e34d1fe0 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/commands/service/CommandSourceServiceTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/commands/service/CommandSourceServiceTest.java @@ -28,6 +28,7 @@ import org.apache.fineract.batch.exception.ErrorInfo; import org.apache.fineract.commands.domain.CommandSource; import org.apache.fineract.commands.domain.CommandSourceRepository; import org.apache.fineract.commands.domain.CommandWrapper; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.infrastructure.codes.exception.CodeNotFoundException; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.api.JsonCommand; @@ -113,4 +114,395 @@ public class CommandSourceServiceTest { Assertions.assertEquals(1001, result.getErrorCode()); Assertions.assertTrue(result.getMessage().contains("Code with name `foo` does not exist")); } + + @Test + public void testProcessCommand_NonGLAccount_SuperUser_BypassesMakerChecker() { + NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class); + JsonCommand jsonCommand = Mockito.mock(JsonCommand.class); + CommandSource commandSource = Mockito.mock(CommandSource.class); + AppUser superUser = Mockito.mock(AppUser.class); + + Mockito.when(commandSource.getPermissionCode()).thenReturn("WITHDRAW_SAVINGSACCOUNT"); + Mockito.when(commandSource.getEntityName()).thenReturn("SAVINGSACCOUNT"); + Mockito.when(commandSource.getActionName()).thenReturn("WITHDRAW"); + Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("WITHDRAW_SAVINGSACCOUNT")).thenReturn(true); + Mockito.when(superUser.isCheckerSuperUser()).thenReturn(true); + + org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder() + .withCommandId(1L).build(); + Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult); + + org.apache.fineract.infrastructure.core.data.CommandProcessingResult result = underTest.processCommand(handler, jsonCommand, + commandSource, superUser, false); + + Assertions.assertNotNull(result); + Mockito.verify(commandSource).markAsChecked(superUser); + Mockito.verify(commandSource, Mockito.never()).markAsAwaitingApproval(); + } + + @Test + public void testProcessCommand_GLAccount_SuperUser_EnforcesMakerChecker_Create() { + NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class); + JsonCommand jsonCommand = Mockito.mock(JsonCommand.class); + CommandSource commandSource = Mockito.mock(CommandSource.class); + AppUser superUser = Mockito.mock(AppUser.class); + + Mockito.when(commandSource.getPermissionCode()).thenReturn("CREATE_GLACCOUNT"); + Mockito.when(commandSource.getEntityName()).thenReturn("GLACCOUNT"); + Mockito.when(commandSource.getActionName()).thenReturn("CREATE"); + Mockito.when(commandSource.getId()).thenReturn(100L); + Mockito.when(commandSource.getResourceId()).thenReturn(200L); + Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("CREATE_GLACCOUNT")).thenReturn(true); + Mockito.when(superUser.isCheckerSuperUser()).thenReturn(true); + + org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder() + .withCommandId(1L).build(); + Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult); + + Assertions.assertThrows(org.apache.fineract.commands.exception.RollbackTransactionNotApprovedException.class, () -> { + underTest.processCommand(handler, jsonCommand, commandSource, superUser, false); + }); + + Mockito.verify(commandSource).markAsAwaitingApproval(); + Mockito.verify(commandSource, Mockito.never()).markAsChecked(any()); + } + + @Test + public void testProcessCommand_GLAccount_SuperUser_EnforcesMakerChecker_Update_And_Disable() { + NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class); + JsonCommand jsonCommand = Mockito.mock(JsonCommand.class); + CommandSource commandSource = Mockito.mock(CommandSource.class); + AppUser superUser = Mockito.mock(AppUser.class); + + Mockito.when(commandSource.getPermissionCode()).thenReturn("UPDATE_GLACCOUNT"); + Mockito.when(commandSource.getEntityName()).thenReturn("GLACCOUNT"); + Mockito.when(commandSource.getActionName()).thenReturn("UPDATE"); + Mockito.when(commandSource.getId()).thenReturn(101L); + Mockito.when(commandSource.getResourceId()).thenReturn(201L); + Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("UPDATE_GLACCOUNT")).thenReturn(true); + Mockito.when(superUser.isCheckerSuperUser()).thenReturn(true); + + org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder() + .withCommandId(1L).build(); + Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult); + + Assertions.assertThrows(org.apache.fineract.commands.exception.RollbackTransactionNotApprovedException.class, () -> { + underTest.processCommand(handler, jsonCommand, commandSource, superUser, false); + }); + + Mockito.verify(commandSource).markAsAwaitingApproval(); + Mockito.verify(commandSource, Mockito.never()).markAsChecked(any()); + } + + @Test + public void testProcessCommand_GLAccount_SuperUser_EnforcesMakerChecker_Delete() { + NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class); + JsonCommand jsonCommand = Mockito.mock(JsonCommand.class); + CommandSource commandSource = Mockito.mock(CommandSource.class); + AppUser superUser = Mockito.mock(AppUser.class); + + Mockito.when(commandSource.getPermissionCode()).thenReturn("DELETE_GLACCOUNT"); + Mockito.when(commandSource.getEntityName()).thenReturn("GLACCOUNT"); + Mockito.when(commandSource.getActionName()).thenReturn("DELETE"); + Mockito.when(commandSource.getId()).thenReturn(102L); + Mockito.when(commandSource.getResourceId()).thenReturn(202L); + Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("DELETE_GLACCOUNT")).thenReturn(true); + Mockito.when(superUser.isCheckerSuperUser()).thenReturn(true); + + org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder() + .withCommandId(1L).build(); + Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult); + + Assertions.assertThrows(org.apache.fineract.commands.exception.RollbackTransactionNotApprovedException.class, () -> { + underTest.processCommand(handler, jsonCommand, commandSource, superUser, false); + }); + + Mockito.verify(commandSource).markAsAwaitingApproval(); + Mockito.verify(commandSource, Mockito.never()).markAsChecked(any()); + } + + @Test + public void testProcessCommand_GLAccount_CheckerApproval_ExecutesSuccessfully() { + NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class); + JsonCommand jsonCommand = Mockito.mock(JsonCommand.class); + CommandSource commandSource = Mockito.mock(CommandSource.class); + AppUser checkerUser = Mockito.mock(AppUser.class); + + Mockito.when(commandSource.getPermissionCode()).thenReturn("UPDATE_GLACCOUNT"); + Mockito.when(commandSource.getEntityName()).thenReturn("GLACCOUNT"); + Mockito.when(commandSource.getActionName()).thenReturn("UPDATE"); + Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("UPDATE_GLACCOUNT")).thenReturn(true); + Mockito.when(checkerUser.isCheckerSuperUser()).thenReturn(false); + + org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder() + .withCommandId(1L).build(); + Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult); + + org.apache.fineract.infrastructure.core.data.CommandProcessingResult result = underTest.processCommand(handler, jsonCommand, + commandSource, checkerUser, true); + + Assertions.assertNotNull(result); + Mockito.verify(commandSource).markAsChecked(checkerUser); + Mockito.verify(commandSource, Mockito.never()).markAsAwaitingApproval(); + } + + @Test + public void testProcessCommand_GLAccount_NonSuperUser_EnforcesMakerChecker() { + NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class); + JsonCommand jsonCommand = Mockito.mock(JsonCommand.class); + CommandSource commandSource = Mockito.mock(CommandSource.class); + AppUser regularUser = Mockito.mock(AppUser.class); + + Mockito.when(commandSource.getPermissionCode()).thenReturn("CREATE_GLACCOUNT"); + Mockito.when(commandSource.getEntityName()).thenReturn("GLACCOUNT"); + Mockito.when(commandSource.getActionName()).thenReturn("CREATE"); + Mockito.when(commandSource.getId()).thenReturn(103L); + Mockito.when(commandSource.getResourceId()).thenReturn(203L); + Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("CREATE_GLACCOUNT")).thenReturn(true); + Mockito.when(regularUser.isCheckerSuperUser()).thenReturn(false); + + org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder() + .withCommandId(1L).build(); + Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult); + + Assertions.assertThrows(org.apache.fineract.commands.exception.RollbackTransactionNotApprovedException.class, () -> { + underTest.processCommand(handler, jsonCommand, commandSource, regularUser, false); + }); + + Mockito.verify(commandSource).markAsAwaitingApproval(); + Mockito.verify(commandSource, Mockito.never()).markAsChecked(any()); + } + + @Test + public void testProcessCommand_CBSBaseInterestRate_SuperUser_EnforcesMakerChecker() { + NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class); + JsonCommand jsonCommand = Mockito.mock(JsonCommand.class); + CommandSource commandSource = Mockito.mock(CommandSource.class); + AppUser superUser = Mockito.mock(AppUser.class); + + Mockito.when(commandSource.getPermissionCode()).thenReturn("CREATE_CBSBASEINTERESTRATE"); + Mockito.when(commandSource.getEntityName()).thenReturn("CBSBASEINTERESTRATE"); + Mockito.when(commandSource.getActionName()).thenReturn("CREATE"); + Mockito.when(commandSource.getId()).thenReturn(104L); + Mockito.when(commandSource.getResourceId()).thenReturn(204L); + Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("CREATE_CBSBASEINTERESTRATE")).thenReturn(true); + Mockito.when(superUser.isCheckerSuperUser()).thenReturn(true); + + org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder() + .withCommandId(1L).build(); + Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult); + + Assertions.assertThrows(org.apache.fineract.commands.exception.RollbackTransactionNotApprovedException.class, () -> { + underTest.processCommand(handler, jsonCommand, commandSource, superUser, false); + }); + + Mockito.verify(commandSource).markAsAwaitingApproval(); + Mockito.verify(commandSource, Mockito.never()).markAsChecked(any()); + } + + @Test + public void testProcessCommand_CBSBaseInterestRate_CheckerApproval_MarksAsChecked() { + NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class); + JsonCommand jsonCommand = Mockito.mock(JsonCommand.class); + CommandSource commandSource = Mockito.mock(CommandSource.class); + AppUser checkerUser = Mockito.mock(AppUser.class); + + Mockito.when(commandSource.getPermissionCode()).thenReturn("CREATE_CBSBASEINTERESTRATE"); + Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("CREATE_CBSBASEINTERESTRATE")).thenReturn(true); + + org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder() + .withCommandId(1L).build(); + Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult); + + org.apache.fineract.infrastructure.core.data.CommandProcessingResult result = underTest.processCommand(handler, jsonCommand, + commandSource, checkerUser, true); + + Assertions.assertNotNull(result); + Mockito.verify(commandSource).markAsChecked(checkerUser); + Mockito.verify(commandSource, Mockito.never()).markAsAwaitingApproval(); + } + + @Test + public void testProcessCommand_Charge_SuperUser_EnforcesMakerChecker() { + NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class); + JsonCommand jsonCommand = Mockito.mock(JsonCommand.class); + CommandSource commandSource = Mockito.mock(CommandSource.class); + AppUser superUser = Mockito.mock(AppUser.class); + + Mockito.when(commandSource.getPermissionCode()).thenReturn("CREATE_CHARGE"); + Mockito.when(commandSource.getEntityName()).thenReturn("CHARGE"); + Mockito.when(commandSource.getActionName()).thenReturn("CREATE"); + Mockito.when(commandSource.getId()).thenReturn(105L); + Mockito.when(commandSource.getResourceId()).thenReturn(205L); + Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("CREATE_CHARGE")).thenReturn(true); + Mockito.when(superUser.isCheckerSuperUser()).thenReturn(true); + + org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder() + .withCommandId(1L).build(); + Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult); + + Assertions.assertThrows(org.apache.fineract.commands.exception.RollbackTransactionNotApprovedException.class, () -> { + underTest.processCommand(handler, jsonCommand, commandSource, superUser, false); + }); + + Mockito.verify(commandSource).markAsAwaitingApproval(); + Mockito.verify(commandSource, Mockito.never()).markAsChecked(any()); + } + + @Test + public void testProcessCommand_Charge_NonSuperUser_EnforcesMakerChecker() { + NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class); + JsonCommand jsonCommand = Mockito.mock(JsonCommand.class); + CommandSource commandSource = Mockito.mock(CommandSource.class); + AppUser regularUser = Mockito.mock(AppUser.class); + + Mockito.when(commandSource.getPermissionCode()).thenReturn("CREATE_CHARGE"); + Mockito.when(commandSource.getEntityName()).thenReturn("CHARGE"); + Mockito.when(commandSource.getActionName()).thenReturn("CREATE"); + Mockito.when(commandSource.getId()).thenReturn(106L); + Mockito.when(commandSource.getResourceId()).thenReturn(206L); + Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("CREATE_CHARGE")).thenReturn(true); + Mockito.when(regularUser.isCheckerSuperUser()).thenReturn(false); + + org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder() + .withCommandId(1L).build(); + Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult); + + Assertions.assertThrows(org.apache.fineract.commands.exception.RollbackTransactionNotApprovedException.class, () -> { + underTest.processCommand(handler, jsonCommand, commandSource, regularUser, false); + }); + + Mockito.verify(commandSource).markAsAwaitingApproval(); + Mockito.verify(commandSource, Mockito.never()).markAsChecked(any()); + } + + @Test + public void testProcessCommand_LoanProduct_SuperUser_EnforcesMakerChecker() { + NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class); + JsonCommand jsonCommand = Mockito.mock(JsonCommand.class); + CommandSource commandSource = Mockito.mock(CommandSource.class); + AppUser superUser = Mockito.mock(AppUser.class); + + Mockito.when(commandSource.getPermissionCode()).thenReturn("CREATE_LOANPRODUCT"); + Mockito.when(commandSource.getEntityName()).thenReturn("LOANPRODUCT"); + Mockito.when(commandSource.getActionName()).thenReturn("CREATE"); + Mockito.when(commandSource.getId()).thenReturn(110L); + Mockito.when(commandSource.getResourceId()).thenReturn(210L); + Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("CREATE_LOANPRODUCT")).thenReturn(true); + Mockito.when(superUser.isCheckerSuperUser()).thenReturn(true); + + org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder() + .withCommandId(1L).build(); + Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult); + + Assertions.assertThrows(org.apache.fineract.commands.exception.RollbackTransactionNotApprovedException.class, () -> { + underTest.processCommand(handler, jsonCommand, commandSource, superUser, false); + }); + + Mockito.verify(commandSource).markAsAwaitingApproval(); + Mockito.verify(commandSource, Mockito.never()).markAsChecked(any()); + } + + @Test + public void testProcessCommand_SavingsProduct_SuperUser_EnforcesMakerChecker() { + NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class); + JsonCommand jsonCommand = Mockito.mock(JsonCommand.class); + CommandSource commandSource = Mockito.mock(CommandSource.class); + AppUser superUser = Mockito.mock(AppUser.class); + + Mockito.when(commandSource.getPermissionCode()).thenReturn("CREATE_SAVINGSPRODUCT"); + Mockito.when(commandSource.getEntityName()).thenReturn("SAVINGSPRODUCT"); + Mockito.when(commandSource.getActionName()).thenReturn("CREATE"); + Mockito.when(commandSource.getId()).thenReturn(111L); + Mockito.when(commandSource.getResourceId()).thenReturn(211L); + Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("CREATE_SAVINGSPRODUCT")).thenReturn(true); + Mockito.when(superUser.isCheckerSuperUser()).thenReturn(true); + + org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder() + .withCommandId(1L).build(); + Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult); + + Assertions.assertThrows(org.apache.fineract.commands.exception.RollbackTransactionNotApprovedException.class, () -> { + underTest.processCommand(handler, jsonCommand, commandSource, superUser, false); + }); + + Mockito.verify(commandSource).markAsAwaitingApproval(); + Mockito.verify(commandSource, Mockito.never()).markAsChecked(any()); + } + + @Test + public void testProcessCommand_CurrentProduct_SuperUser_EnforcesMakerChecker() { + NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class); + JsonCommand jsonCommand = Mockito.mock(JsonCommand.class); + CommandSource commandSource = Mockito.mock(CommandSource.class); + AppUser superUser = Mockito.mock(AppUser.class); + + Mockito.when(commandSource.getPermissionCode()).thenReturn("CREATE_CURRENTPRODUCT"); + Mockito.when(commandSource.getEntityName()).thenReturn("CURRENTPRODUCT"); + Mockito.when(commandSource.getActionName()).thenReturn("CREATE"); + Mockito.when(commandSource.getId()).thenReturn(112L); + Mockito.when(commandSource.getResourceId()).thenReturn(212L); + Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("CREATE_CURRENTPRODUCT")).thenReturn(true); + Mockito.when(superUser.isCheckerSuperUser()).thenReturn(true); + + org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder() + .withCommandId(1L).build(); + Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult); + + Assertions.assertThrows(org.apache.fineract.commands.exception.RollbackTransactionNotApprovedException.class, () -> { + underTest.processCommand(handler, jsonCommand, commandSource, superUser, false); + }); + + Mockito.verify(commandSource).markAsAwaitingApproval(); + Mockito.verify(commandSource, Mockito.never()).markAsChecked(any()); + } + + @Test + public void testProcessCommand_ShareProduct_SuperUser_EnforcesMakerChecker() { + NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class); + JsonCommand jsonCommand = Mockito.mock(JsonCommand.class); + CommandSource commandSource = Mockito.mock(CommandSource.class); + AppUser superUser = Mockito.mock(AppUser.class); + + Mockito.when(commandSource.getPermissionCode()).thenReturn("CREATE_SHAREPRODUCT"); + Mockito.when(commandSource.getEntityName()).thenReturn("SHAREPRODUCT"); + Mockito.when(commandSource.getActionName()).thenReturn("CREATE"); + Mockito.when(commandSource.getId()).thenReturn(113L); + Mockito.when(commandSource.getResourceId()).thenReturn(213L); + Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("CREATE_SHAREPRODUCT")).thenReturn(true); + Mockito.when(superUser.isCheckerSuperUser()).thenReturn(true); + + org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder() + .withCommandId(1L).build(); + Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult); + + Assertions.assertThrows(org.apache.fineract.commands.exception.RollbackTransactionNotApprovedException.class, () -> { + underTest.processCommand(handler, jsonCommand, commandSource, superUser, false); + }); + + Mockito.verify(commandSource).markAsAwaitingApproval(); + Mockito.verify(commandSource, Mockito.never()).markAsChecked(any()); + } + + @Test + public void testProcessCommand_Products_CheckerApproval_MarksAsChecked() { + NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class); + JsonCommand jsonCommand = Mockito.mock(JsonCommand.class); + CommandSource commandSource = Mockito.mock(CommandSource.class); + AppUser checkerUser = Mockito.mock(AppUser.class); + + Mockito.when(commandSource.getPermissionCode()).thenReturn("CREATE_LOANPRODUCT"); + Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("CREATE_LOANPRODUCT")).thenReturn(true); + + org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder() + .withCommandId(1L).build(); + Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult); + + org.apache.fineract.infrastructure.core.data.CommandProcessingResult result = underTest.processCommand(handler, jsonCommand, + commandSource, checkerUser, true); + + Assertions.assertNotNull(result); + Mockito.verify(commandSource).markAsChecked(checkerUser); + Mockito.verify(commandSource, Mockito.never()).markAsAwaitingApproval(); + } } diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/charge/handler/CreateChargeDefinitionCommandHandlerTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/charge/handler/CreateChargeDefinitionCommandHandlerTest.java new file mode 100644 index 000000000..1f5b7bcde --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/charge/handler/CreateChargeDefinitionCommandHandlerTest.java @@ -0,0 +1,60 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.charge.handler; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +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.portfolio.charge.service.ChargeWritePlatformService; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class CreateChargeDefinitionCommandHandlerTest { + + @Mock + private ChargeWritePlatformService writePlatformService; + + @Mock + private JsonCommand command; + + @InjectMocks + private CreateChargeDefinitionCommandHandler underTest; + + @Test + void shouldDelegateToWritePlatformServiceAndReturnResult() { + CommandProcessingResult expectedResult = new CommandProcessingResultBuilder() + .withCommandId(101L) + .withEntityId(201L) + .build(); + when(writePlatformService.createCharge(command)).thenReturn(expectedResult); + + CommandProcessingResult actualResult = underTest.processCommand(command); + + verify(writePlatformService).createCharge(command); + assertEquals(expectedResult, actualResult); + } +} diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanproduct/handler/CreateLoanProductCommandHandlerTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanproduct/handler/CreateLoanProductCommandHandlerTest.java new file mode 100644 index 000000000..c161a276f --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanproduct/handler/CreateLoanProductCommandHandlerTest.java @@ -0,0 +1,60 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanproduct.handler; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +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.portfolio.loanproduct.service.LoanProductWritePlatformService; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class CreateLoanProductCommandHandlerTest { + + @Mock + private LoanProductWritePlatformService writePlatformService; + + @Mock + private JsonCommand command; + + @InjectMocks + private CreateLoanProductCommandHandler underTest; + + @Test + void shouldDelegateToWritePlatformServiceAndReturnResult() { + CommandProcessingResult expectedResult = new CommandProcessingResultBuilder() + .withCommandId(101L) + .withEntityId(201L) + .build(); + when(writePlatformService.createLoanProduct(command)).thenReturn(expectedResult); + + CommandProcessingResult actualResult = underTest.processCommand(command); + + verify(writePlatformService).createLoanProduct(command); + assertEquals(expectedResult, actualResult); + } +} diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/savings/handler/CreateCbsBaseInterestRateCommandHandlerTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/savings/handler/CreateCbsBaseInterestRateCommandHandlerTest.java new file mode 100644 index 000000000..742a06d06 --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/savings/handler/CreateCbsBaseInterestRateCommandHandlerTest.java @@ -0,0 +1,60 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.savings.handler; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +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.portfolio.savings.service.FdBaseInterestRateWritePlatformService; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class CreateCbsBaseInterestRateCommandHandlerTest { + + @Mock + private FdBaseInterestRateWritePlatformService writePlatformService; + + @Mock + private JsonCommand command; + + @InjectMocks + private CreateCbsBaseInterestRateCommandHandler underTest; + + @Test + void shouldDelegateToWritePlatformServiceAndReturnResult() { + CommandProcessingResult expectedResult = new CommandProcessingResultBuilder() + .withCommandId(100L) + .withEntityId(200L) + .build(); + when(writePlatformService.saveInterestRates(command)).thenReturn(expectedResult); + + CommandProcessingResult actualResult = underTest.processCommand(command); + + verify(writePlatformService).saveInterestRates(command); + assertEquals(expectedResult, actualResult); + } +} diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/savings/handler/CreateSavingsProductCommandHandlerTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/savings/handler/CreateSavingsProductCommandHandlerTest.java new file mode 100644 index 000000000..a5071b3cd --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/savings/handler/CreateSavingsProductCommandHandlerTest.java @@ -0,0 +1,60 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.savings.handler; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +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.portfolio.savings.service.SavingsProductWritePlatformService; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class CreateSavingsProductCommandHandlerTest { + + @Mock + private SavingsProductWritePlatformService writePlatformService; + + @Mock + private JsonCommand command; + + @InjectMocks + private CreateSavingsProductCommandHandler underTest; + + @Test + void shouldDelegateToWritePlatformServiceAndReturnResult() { + CommandProcessingResult expectedResult = new CommandProcessingResultBuilder() + .withCommandId(102L) + .withEntityId(202L) + .build(); + when(writePlatformService.create(command)).thenReturn(expectedResult); + + CommandProcessingResult actualResult = underTest.processCommand(command); + + verify(writePlatformService).create(command); + assertEquals(expectedResult, actualResult); + } +} diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/savings/handler/current/CreateCurrentProductCommandHandlerTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/savings/handler/current/CreateCurrentProductCommandHandlerTest.java new file mode 100644 index 000000000..ed9d72e80 --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/savings/handler/current/CreateCurrentProductCommandHandlerTest.java @@ -0,0 +1,60 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.savings.handler.current; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +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.portfolio.currentproduct.service.CurrentProductWritePlatformService; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class CreateCurrentProductCommandHandlerTest { + + @Mock + private CurrentProductWritePlatformService writePlatformService; + + @Mock + private JsonCommand command; + + @InjectMocks + private CreateCurrentProductCommandHandler underTest; + + @Test + void shouldDelegateToWritePlatformServiceAndReturnResult() { + CommandProcessingResult expectedResult = new CommandProcessingResultBuilder() + .withCommandId(103L) + .withEntityId(203L) + .build(); + when(writePlatformService.create(command)).thenReturn(expectedResult); + + CommandProcessingResult actualResult = underTest.processCommand(command); + + verify(writePlatformService).create(command); + assertEquals(expectedResult, actualResult); + } +} diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/savings/service/FdBaseInterestRateReadPlatformServiceImplTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/savings/service/FdBaseInterestRateReadPlatformServiceImplTest.java new file mode 100644 index 000000000..067ccdeb9 --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/savings/service/FdBaseInterestRateReadPlatformServiceImplTest.java @@ -0,0 +1,265 @@ +/** + * 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.savings.service; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.math.BigDecimal; +import java.util.Collections; +import java.util.List; +import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.apache.fineract.portfolio.savings.api.FdInterestRateSaveRequest; +import org.apache.fineract.portfolio.savings.domain.FdBaseInterestRateEffective; +import org.apache.fineract.portfolio.savings.domain.FdBaseInterestRateEffectiveRepository; +import org.apache.fineract.portfolio.savings.domain.FdBaseInterestRateSlabHistoryRepository; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class FdBaseInterestRateReadPlatformServiceImplTest { + + @Mock + private FdBaseInterestRateEffectiveRepository effectiveRepository; + + @Mock + private FdBaseInterestRateSlabHistoryRepository slabRepository; + + @InjectMocks + private FdBaseInterestRateReadPlatformServiceImpl service; + + @BeforeEach + void setUp() { + ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null)); + } + + @AfterEach + void tearDown() { + ThreadLocalContextUtil.setTenant(null); + } + + private FdInterestRateSaveRequest.SlabRequest createSlab(int srNo, String desc, int periodType, int from, Integer to, double rate, double seniorRate) { + FdInterestRateSaveRequest.SlabRequest s = new FdInterestRateSaveRequest.SlabRequest(); + s.setSrNo(srNo); + s.setDescription(desc); + s.setPeriodTypeEnum(periodType); + s.setFromPeriod(from); + s.setToPeriod(to); + s.setAnnualInterestRate(BigDecimal.valueOf(rate)); + s.setSeniorCitizenRate(BigDecimal.valueOf(seniorRate)); + return s; + } + + @Test + void testValidSingleDepositSlabs() { + FdInterestRateSaveRequest request = new FdInterestRateSaveRequest(); + request.setFdType("SINGLE"); + request.setVersionLabel("v1.0"); + + List slabs = List.of( + createSlab(1, "1 to 29 Days", 0, 1, 29, 4.5, 5.0), + createSlab(2, "30 to 89 Days", 0, 30, 89, 5.0, 5.5), + createSlab(3, "90 to 179 Days", 0, 90, 179, 5.5, 6.0), + createSlab(4, "180+ Days", 0, 180, null, 6.0, 6.5) + ); + request.setSlabs(slabs); + + when(effectiveRepository.findByFdTypeAndIsActiveTrueOrderByEffectiveFromDateDesc("SINGLE")) + .thenReturn(Collections.emptyList()); + when(effectiveRepository.saveAndFlush(any())).thenAnswer(invocation -> invocation.getArgument(0)); + + assertDoesNotThrow(() -> service.saveInterestRates(request, 1L)); + verify(effectiveRepository).saveAndFlush(any()); + } + + @Test + void testValidDoubleDepositSlabs() { + FdInterestRateSaveRequest request = new FdInterestRateSaveRequest(); + request.setFdType("DOUBLE"); + request.setVersionLabel("v1.0"); + + List slabs = List.of( + createSlab(1, "1 to 2 Years", 3, 1, 2, 6.5, 7.0), + createSlab(2, "3 to 5 Years", 3, 3, 5, 6.75, 7.25), + createSlab(3, "6 to 10 Years", 3, 6, 10, 7.0, 7.5), + createSlab(4, "11+ Years", 3, 11, null, 7.25, 7.75) + ); + request.setSlabs(slabs); + + when(effectiveRepository.findByFdTypeAndIsActiveTrueOrderByEffectiveFromDateDesc("DOUBLE")) + .thenReturn(Collections.emptyList()); + when(effectiveRepository.saveAndFlush(any())).thenAnswer(invocation -> invocation.getArgument(0)); + + assertDoesNotThrow(() -> service.saveInterestRates(request, 1L)); + } + + @Test + void testInvalidFirstSlabDoesNotStartAtOne() { + FdInterestRateSaveRequest request = new FdInterestRateSaveRequest(); + request.setFdType("SINGLE"); + List slabs = List.of( + createSlab(1, "2 to 29 Days", 0, 2, 29, 4.5, 5.0), + createSlab(2, "30+ Days", 0, 30, null, 5.0, 5.5) + ); + request.setSlabs(slabs); + + PlatformApiDataValidationException ex = assertThrows(PlatformApiDataValidationException.class, + () -> service.saveInterestRates(request, 1L)); + assertEquals(1, ex.getErrors().size()); + assertEquals("validation.msg.FD_BASE_INTEREST_RATE_VERSION.slabs[1].fromPeriod.first.slab.must.start.at.one", + ex.getErrors().get(0).getUserMessageGlobalisationCode()); + } + + @Test + void testInvalidOverlapRanges() { + FdInterestRateSaveRequest request = new FdInterestRateSaveRequest(); + request.setFdType("SINGLE"); + List slabs = List.of( + createSlab(1, "1 to 29 Days", 0, 1, 29, 4.5, 5.0), + createSlab(2, "20 to 50 Days", 0, 20, 50, 5.0, 5.5) + ); + request.setSlabs(slabs); + + PlatformApiDataValidationException ex = assertThrows(PlatformApiDataValidationException.class, + () -> service.saveInterestRates(request, 1L)); + assertEquals("validation.msg.FD_BASE_INTEREST_RATE_VERSION.slabs[2].fromPeriod.must.not.overlap.previous.range", + ex.getErrors().get(0).getUserMessageGlobalisationCode()); + } + + @Test + void testInvalidGapBetweenRanges() { + FdInterestRateSaveRequest request = new FdInterestRateSaveRequest(); + request.setFdType("SINGLE"); + List slabs = List.of( + createSlab(1, "1 to 29 Days", 0, 1, 29, 4.5, 5.0), + createSlab(2, "35 to 89 Days", 0, 35, 89, 5.0, 5.5) + ); + request.setSlabs(slabs); + + PlatformApiDataValidationException ex = assertThrows(PlatformApiDataValidationException.class, + () -> service.saveInterestRates(request, 1L)); + assertEquals("validation.msg.FD_BASE_INTEREST_RATE_VERSION.slabs[2].fromPeriod.must.be.continuous.with.previous.range", + ex.getErrors().get(0).getUserMessageGlobalisationCode()); + } + + @Test + void testInvalidDuplicateRanges() { + FdInterestRateSaveRequest request = new FdInterestRateSaveRequest(); + request.setFdType("SINGLE"); + List slabs = List.of( + createSlab(1, "1 to 29 Days", 0, 1, 29, 4.5, 5.0), + createSlab(2, "1 to 29 Days duplicate", 0, 1, 29, 5.0, 5.5) + ); + request.setSlabs(slabs); + + PlatformApiDataValidationException ex = assertThrows(PlatformApiDataValidationException.class, + () -> service.saveInterestRates(request, 1L)); + assertEquals("validation.msg.FD_BASE_INTEREST_RATE_VERSION.slabs[2].fromPeriod.must.not.overlap.previous.range", + ex.getErrors().get(0).getUserMessageGlobalisationCode()); + } + + @Test + void testInvalidFromGreaterThanTo() { + FdInterestRateSaveRequest request = new FdInterestRateSaveRequest(); + request.setFdType("SINGLE"); + List slabs = List.of( + createSlab(1, "1 to 20 Days", 0, 1, 20, 4.5, 5.0), + createSlab(2, "21 to 10 Days", 0, 21, 10, 5.0, 5.5) + ); + request.setSlabs(slabs); + + PlatformApiDataValidationException ex = assertThrows(PlatformApiDataValidationException.class, + () -> service.saveInterestRates(request, 1L)); + assertEquals("validation.msg.FD_BASE_INTEREST_RATE_VERSION.slabs[2].toPeriod.cannot.be.less.than.fromPeriod", + ex.getErrors().get(0).getUserMessageGlobalisationCode()); + } + + @Test + void testInvalidBlankToPeriodOnIntermediateSlab() { + FdInterestRateSaveRequest request = new FdInterestRateSaveRequest(); + request.setFdType("SINGLE"); + List slabs = List.of( + createSlab(1, "1 to null Days", 0, 1, null, 4.5, 5.0), + createSlab(2, "30+ Days", 0, 30, null, 5.0, 5.5) + ); + request.setSlabs(slabs); + + PlatformApiDataValidationException ex = assertThrows(PlatformApiDataValidationException.class, + () -> service.saveInterestRates(request, 1L)); + assertEquals("validation.msg.FD_BASE_INTEREST_RATE_VERSION.slabs[1].toPeriod.cannot.be.blank.for.intermediate.slab", + ex.getErrors().get(0).getUserMessageGlobalisationCode()); + } + + @Test + void testInvalidMixedPeriodTypes() { + FdInterestRateSaveRequest request = new FdInterestRateSaveRequest(); + request.setFdType("SINGLE"); + List slabs = List.of( + createSlab(1, "1 to 29 Days", 0, 1, 29, 4.5, 5.0), + createSlab(2, "30 to 89 Months", 2, 30, 89, 5.0, 5.5) + ); + request.setSlabs(slabs); + + PlatformApiDataValidationException ex = assertThrows(PlatformApiDataValidationException.class, + () -> service.saveInterestRates(request, 1L)); + assertEquals("validation.msg.FD_BASE_INTEREST_RATE_VERSION.slabs[2].periodTypeEnum.must.be.consistent.across.all.slabs", + ex.getErrors().get(0).getUserMessageGlobalisationCode()); + } + + @Test + void testInvalidNegativeInterestRate() { + FdInterestRateSaveRequest request = new FdInterestRateSaveRequest(); + request.setFdType("SINGLE"); + List slabs = List.of( + createSlab(1, "1 to 29 Days", 0, 1, 29, -1.0, 5.0) + ); + request.setSlabs(slabs); + + PlatformApiDataValidationException ex = assertThrows(PlatformApiDataValidationException.class, + () -> service.saveInterestRates(request, 1L)); + assertEquals("validation.msg.FD_BASE_INTEREST_RATE_VERSION.slabs[1].annualInterestRate.must.be.non.negative", + ex.getErrors().get(0).getUserMessageGlobalisationCode()); + } + + @Test + void testInvalidNegativeSeniorRate() { + FdInterestRateSaveRequest request = new FdInterestRateSaveRequest(); + request.setFdType("SINGLE"); + List slabs = List.of( + createSlab(1, "1 to 29 Days", 0, 1, 29, 4.5, -0.5) + ); + request.setSlabs(slabs); + + PlatformApiDataValidationException ex = assertThrows(PlatformApiDataValidationException.class, + () -> service.saveInterestRates(request, 1L)); + assertEquals("validation.msg.FD_BASE_INTEREST_RATE_VERSION.slabs[1].seniorCitizenRate.must.be.non.negative", + ex.getErrors().get(0).getUserMessageGlobalisationCode()); + } +} diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/savings/service/FdBaseInterestRateWritePlatformServiceImplTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/savings/service/FdBaseInterestRateWritePlatformServiceImplTest.java new file mode 100644 index 000000000..3da1c04de --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/savings/service/FdBaseInterestRateWritePlatformServiceImplTest.java @@ -0,0 +1,80 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.savings.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.savings.api.FdInterestRateSaveRequest; +import org.apache.fineract.portfolio.savings.data.FdInterestRateEffectiveData; +import org.apache.fineract.useradministration.domain.AppUser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class FdBaseInterestRateWritePlatformServiceImplTest { + + @Mock + private PlatformSecurityContext context; + + @Mock + private FdBaseInterestRateReadPlatformService readPlatformService; + + @Mock + private JsonCommand command; + + @Mock + private AppUser appUser; + + @InjectMocks + private FdBaseInterestRateWritePlatformServiceImpl writePlatformService; + + @Test + void shouldParseJsonAndSaveInterestRates() { + String jsonPayload = "{\"fdType\":\"SINGLE\",\"versionLabel\":\"v1.0\",\"slabs\":[{\"srNo\":1,\"description\":\"1 to 29 Days\",\"periodTypeEnum\":0,\"fromPeriod\":1,\"toPeriod\":29,\"annualInterestRate\":5.0,\"seniorCitizenRate\":5.5}]}"; + when(command.json()).thenReturn(jsonPayload); + when(command.commandId()).thenReturn(789L); + when(context.authenticatedUser()).thenReturn(appUser); + when(appUser.getId()).thenReturn(1L); + + FdInterestRateEffectiveData savedData = FdInterestRateEffectiveData.builder() + .id(10L) + .fdType("SINGLE") + .versionLabel("v1.0") + .build(); + when(readPlatformService.saveInterestRates(any(FdInterestRateSaveRequest.class), eq(1L))).thenReturn(savedData); + + CommandProcessingResult result = writePlatformService.saveInterestRates(command); + + assertNotNull(result); + assertEquals(789L, result.getCommandId()); + assertEquals(10L, result.getResourceId()); + verify(readPlatformService).saveInterestRates(any(FdInterestRateSaveRequest.class), eq(1L)); + } +} diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/shareproducts/handler/CreateShareProductCommandHandlerTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/shareproducts/handler/CreateShareProductCommandHandlerTest.java new file mode 100644 index 000000000..ba8154b6d --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/shareproducts/handler/CreateShareProductCommandHandlerTest.java @@ -0,0 +1,60 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.shareproducts.handler; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +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.portfolio.shareproducts.service.ShareProductWritePlatformService; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class CreateShareProductCommandHandlerTest { + + @Mock + private ShareProductWritePlatformService writePlatformService; + + @Mock + private JsonCommand command; + + @InjectMocks + private CreateShareProductCommandHandler underTest; + + @Test + void shouldDelegateToWritePlatformServiceAndReturnResult() { + CommandProcessingResult expectedResult = new CommandProcessingResultBuilder() + .withCommandId(104L) + .withEntityId(204L) + .build(); + when(writePlatformService.createShareProduct(command)).thenReturn(expectedResult); + + CommandProcessingResult actualResult = underTest.processCommand(command); + + verify(writePlatformService).createShareProduct(command); + assertEquals(expectedResult, actualResult); + } +}