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..591e46473 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,25 @@ 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()) + || "FIXEDDEPOSITPRODUCT".equalsIgnoreCase(commandSource.getEntityName()) + || "USER".equalsIgnoreCase(commandSource.getEntityName()) + || "ROLE".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 24adb21c5..a8443f749 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-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java index 18a910266..2529cee37 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java @@ -26,6 +26,8 @@ public interface ConfigurationDomainService { boolean isMakerCheckerEnabledForTask(String taskPermissionCode); + boolean isMakerCheckerEnabled(); + /** * Global kill-switch for MFA-factor-backed login enforcement. When true, users with at least * one verified, non-disabled row in {@code m_user_mfa_factor} must complete a 2FA challenge diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java index 9056094c5..c393095d9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java @@ -61,6 +61,11 @@ public class ConfigurationDomainServiceJpa implements ConfigurationDomainService return false; } + @Override + public boolean isMakerCheckerEnabled() { + return getGlobalConfigurationPropertyData(GlobalConfigurationConstants.MAKER_CHECKER).isEnabled(); + } + @Override public List getAllowedLoanStatusesForExternalAssetTransfer() { final GlobalConfigurationPropertyData property = getGlobalConfigurationPropertyData( 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 180bfc0da..40f9f42c8 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 @@ -170,6 +170,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; @@ -201,6 +203,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/api/UsersApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/useradministration/api/UsersApiResource.java index 794a95eca..57744602b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/useradministration/api/UsersApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/useradministration/api/UsersApiResource.java @@ -88,6 +88,11 @@ public class UsersApiResource { private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; private final BulkImportWorkbookPopulatorService bulkImportWorkbookPopulatorService; private final BulkImportWorkbookService bulkImportWorkbookService; + private final org.apache.fineract.useradministration.service.UserPendingWritePlatformService userPendingWritePlatformService; + private final org.apache.fineract.useradministration.service.UserPendingReadPlatformService userPendingReadPlatformService; + private final org.apache.fineract.infrastructure.core.serialization.FromJsonHelper fromApiJsonHelper; + private final org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService configurationDomainService; + private final DefaultToApiJsonSerializer pendingRequestApiJsonSerializer; @GET @Operation(summary = "Retrieve list of users", description = "Example Requests:\n" + "\n" + "users\n" + "\n" + "\n" @@ -158,6 +163,12 @@ public class UsersApiResource { @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) public String create(@Parameter(hidden = true) final String apiRequestBodyAsJson) { + if (this.configurationDomainService.isMakerCheckerEnabled()) { + final org.apache.fineract.infrastructure.core.api.JsonCommand command = org.apache.fineract.infrastructure.core.api.JsonCommand + .fromJsonElement(null, this.fromApiJsonHelper.parse(apiRequestBodyAsJson), this.fromApiJsonHelper); + final CommandProcessingResult result = this.userPendingWritePlatformService.submitCreateUser(command); + return this.toApiJsonSerializer.serialize(result); + } final CommandWrapper commandRequest = new CommandWrapperBuilder() // .createUser() // @@ -179,6 +190,12 @@ public class UsersApiResource { @Produces({ MediaType.APPLICATION_JSON }) public String update(@PathParam("userId") @Parameter(description = "userId") final Long userId, @Parameter(hidden = true) final String apiRequestBodyAsJson) { + if (this.configurationDomainService.isMakerCheckerEnabled()) { + final org.apache.fineract.infrastructure.core.api.JsonCommand command = org.apache.fineract.infrastructure.core.api.JsonCommand + .fromJsonElement(userId, this.fromApiJsonHelper.parse(apiRequestBodyAsJson), this.fromApiJsonHelper); + final CommandProcessingResult result = this.userPendingWritePlatformService.submitUpdateUser(userId, command); + return this.toApiJsonSerializer.serialize(result); + } final CommandWrapper commandRequest = new CommandWrapperBuilder() // .updateUser(userId) // @@ -190,6 +207,56 @@ public class UsersApiResource { return this.toApiJsonSerializer.serialize(result); } + @GET + @Path("pending") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "List User creations & edits pending approval") + public org.apache.fineract.infrastructure.core.service.Page retrieveAllPendingUsers( + @QueryParam("status") final String status, + @QueryParam("username") final String username, + @QueryParam("fromDate") final String fromDate, + @QueryParam("toDate") final String toDate, + @QueryParam("offset") final Integer offset, + @QueryParam("limit") final Integer limit) { + this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS); + return this.userPendingReadPlatformService.retrieveAllPendingRequests(status, username, fromDate, toDate, offset, limit); + } + + @GET + @Path("pending/{id}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve a pending user request by ID") + public String retrievePendingUser(@PathParam("id") final Long id, @Context final UriInfo uriInfo) { + this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS); + final org.apache.fineract.useradministration.data.UserPendingRequestData data = this.userPendingReadPlatformService.retrievePendingRequest(id); + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return this.pendingRequestApiJsonSerializer.serialize(settings, data); + } + + @POST + @Path("pending/{id}/approve") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Approve a pending User creation or update") + public String approvePendingUser(@PathParam("id") final Long id) { + final CommandProcessingResult result = this.userPendingWritePlatformService.approvePendingRequest(id); + return this.toApiJsonSerializer.serialize(result); + } + + @POST + @Path("pending/{id}/reject") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Reject a pending User creation or update") + public String rejectPendingUser(@PathParam("id") final Long id, @Parameter(hidden = true) final String apiRequestBodyAsJson) { + final org.apache.fineract.infrastructure.core.api.JsonCommand command = org.apache.fineract.infrastructure.core.api.JsonCommand + .fromJsonElement(id, this.fromApiJsonHelper.parse(apiRequestBodyAsJson), this.fromApiJsonHelper); + final CommandProcessingResult result = this.userPendingWritePlatformService.rejectPendingRequest(id, command); + return this.toApiJsonSerializer.serialize(result); + } + @POST @Path("{userId}/pwd") @Operation(summary = "Change the password of a User", description = "When updating a password you must provide the repeatPassword parameter also.") 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/data/UserPendingRequestData.java b/fineract-provider/src/main/java/org/apache/fineract/useradministration/data/UserPendingRequestData.java new file mode 100644 index 000000000..dcdf066ec --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/useradministration/data/UserPendingRequestData.java @@ -0,0 +1,59 @@ +/** + * 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.useradministration.data; + +import java.io.Serializable; +import java.time.OffsetDateTime; +import java.util.Collection; +import java.util.Map; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class UserPendingRequestData implements Serializable { + + private static final long serialVersionUID = 1L; + + private final Long id; + private final Long userId; + private final String requestType; + private final String status; + private final String username; + private final String firstname; + private final String lastname; + private final String email; + private final String mobileNo; + private final Long officeId; + private final String officeName; + private final Long staffId; + private final String staffName; + private final String otpDeliveryMethod; + private final Boolean passwordNeverExpires; + private final Collection roleNames; + private final Long makerId; + private final String makerUsername; + private final OffsetDateTime submittedDate; + private final Long checkerId; + private final String checkerUsername; + private final OffsetDateTime checkedDate; + private final String rejectReason; + private final Map originalValues; + private final Map proposedValues; +} 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/domain/UserPendingRequest.java b/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/UserPendingRequest.java new file mode 100644 index 000000000..a143d8598 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/UserPendingRequest.java @@ -0,0 +1,113 @@ +/** + * 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.useradministration.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; + +@Entity +@Table(name = "m_user_pending_request") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Setter +public class UserPendingRequest extends AbstractPersistableCustom { + + @Column(name = "user_id") + private Long userId; + + @Column(name = "request_type", nullable = false, length = 20) + private String requestType; + + @Column(name = "status", nullable = false, length = 20) + private String status; + + @Column(name = "payload_json", nullable = false) + private String payloadJson; + + @Column(name = "original_json") + private String originalJson; + + @Column(name = "password_hash") + private String passwordHash; + + @Column(name = "maker_id", nullable = false) + private Long makerId; + + @Column(name = "submitted_date", nullable = false) + private OffsetDateTime submittedDate; + + @Column(name = "checker_id") + private Long checkerId; + + @Column(name = "checked_date") + private OffsetDateTime checkedDate; + + @Column(name = "reject_reason", length = 500) + private String rejectReason; + + @Column(name = "pending_lock_key", length = 100, unique = true) + private String pendingLockKey; + + public static UserPendingRequest create(final UserPendingRequestType requestType, final Long userId, + final String payloadJson, final String originalJson, final String passwordHash, + final Long makerId, final String pendingLockKey) { + final UserPendingRequest request = new UserPendingRequest(); + request.requestType = requestType.name(); + request.userId = userId; + request.status = UserPendingRequestStatus.PENDING.name(); + request.payloadJson = payloadJson; + request.originalJson = originalJson; + request.passwordHash = passwordHash; + request.makerId = makerId; + request.submittedDate = OffsetDateTime.now(ZoneOffset.UTC); + request.pendingLockKey = pendingLockKey; + return request; + } + + public boolean isPending() { + return UserPendingRequestStatus.PENDING.name().equals(this.status); + } + + public boolean isMadeBy(final Long appUserId) { + return this.makerId != null && this.makerId.equals(appUserId); + } + + public void approve(final Long checkerId) { + this.status = UserPendingRequestStatus.APPROVED.name(); + this.checkerId = checkerId; + this.checkedDate = OffsetDateTime.now(ZoneOffset.UTC); + this.pendingLockKey = null; + } + + public void reject(final Long checkerId, final String reason) { + this.status = UserPendingRequestStatus.REJECTED.name(); + this.checkerId = checkerId; + this.checkedDate = OffsetDateTime.now(ZoneOffset.UTC); + this.rejectReason = reason; + this.pendingLockKey = null; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/UserPendingRequestRepository.java b/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/UserPendingRequestRepository.java new file mode 100644 index 000000000..b622c6c3c --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/UserPendingRequestRepository.java @@ -0,0 +1,30 @@ +/** + * 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.useradministration.domain; + +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface UserPendingRequestRepository extends JpaRepository, JpaSpecificationExecutor { + + Optional findByPendingLockKey(String pendingLockKey); + + boolean existsByPendingLockKey(String pendingLockKey); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/UserPendingRequestRepositoryWrapper.java b/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/UserPendingRequestRepositoryWrapper.java new file mode 100644 index 000000000..8259570c3 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/UserPendingRequestRepositoryWrapper.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.useradministration.domain; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.useradministration.exception.UserPendingRequestNotFoundException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class UserPendingRequestRepositoryWrapper { + + private final UserPendingRequestRepository repository; + + @Transactional(readOnly = true) + public UserPendingRequest findOneWithNotFoundDetection(final Long id) { + return this.repository.findById(id).orElseThrow(() -> new UserPendingRequestNotFoundException(id)); + } + + public UserPendingRequest saveAndFlush(final UserPendingRequest request) { + return this.repository.saveAndFlush(request); + } + + public UserPendingRequest save(final UserPendingRequest request) { + return this.repository.save(request); + } + + @Transactional(readOnly = true) + public boolean existsByPendingLockKey(final String pendingLockKey) { + return this.repository.existsByPendingLockKey(pendingLockKey); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/UserPendingRequestStatus.java b/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/UserPendingRequestStatus.java new file mode 100644 index 000000000..2633e4b98 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/UserPendingRequestStatus.java @@ -0,0 +1,37 @@ +/** + * 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.useradministration.domain; + +public enum UserPendingRequestStatus { + PENDING, + APPROVED, + REJECTED; + + public static UserPendingRequestStatus fromString(final String status) { + if (status == null) { + return null; + } + for (final UserPendingRequestStatus requestStatus : values()) { + if (requestStatus.name().equalsIgnoreCase(status)) { + return requestStatus; + } + } + throw new IllegalArgumentException("Unknown UserPendingRequestStatus: " + status); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/UserPendingRequestType.java b/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/UserPendingRequestType.java new file mode 100644 index 000000000..2a9700978 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/UserPendingRequestType.java @@ -0,0 +1,36 @@ +/** + * 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.useradministration.domain; + +public enum UserPendingRequestType { + CREATE_USER, + UPDATE_USER; + + public static UserPendingRequestType fromString(final String type) { + if (type == null) { + return null; + } + for (final UserPendingRequestType requestType : values()) { + if (requestType.name().equalsIgnoreCase(type)) { + return requestType; + } + } + throw new IllegalArgumentException("Unknown UserPendingRequestType: " + type); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/useradministration/exception/UserPendingRequestNotFoundException.java b/fineract-provider/src/main/java/org/apache/fineract/useradministration/exception/UserPendingRequestNotFoundException.java new file mode 100644 index 000000000..c2f5b9823 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/useradministration/exception/UserPendingRequestNotFoundException.java @@ -0,0 +1,28 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.useradministration.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException; + +public class UserPendingRequestNotFoundException extends AbstractPlatformResourceNotFoundException { + + public UserPendingRequestNotFoundException(final Long id) { + super("error.msg.userpendingrequest.id.invalid", "User pending request with identifier " + id + " does not exist", id); + } +} 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/java/org/apache/fineract/useradministration/service/UserPendingReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/useradministration/service/UserPendingReadPlatformService.java new file mode 100644 index 000000000..4dac903d4 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/useradministration/service/UserPendingReadPlatformService.java @@ -0,0 +1,30 @@ +/** + * 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.useradministration.service; + +import org.apache.fineract.infrastructure.core.service.Page; +import org.apache.fineract.useradministration.data.UserPendingRequestData; + +public interface UserPendingReadPlatformService { + + Page retrieveAllPendingRequests(String status, String username, String fromDate, String toDate, + Integer offset, Integer limit); + + UserPendingRequestData retrievePendingRequest(Long id); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/useradministration/service/UserPendingReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/useradministration/service/UserPendingReadPlatformServiceImpl.java new file mode 100644 index 000000000..08d2f1038 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/useradministration/service/UserPendingReadPlatformServiceImpl.java @@ -0,0 +1,329 @@ +/** + * 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.useradministration.service; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.infrastructure.core.domain.JdbcSupport; +import org.apache.fineract.infrastructure.core.serialization.GoogleGsonSerializerHelper; +import org.apache.fineract.infrastructure.core.service.Page; +import org.apache.fineract.useradministration.data.UserPendingRequestData; +import org.apache.fineract.useradministration.domain.UserPendingRequest; +import org.apache.fineract.useradministration.domain.UserPendingRequestRepositoryWrapper; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class UserPendingReadPlatformServiceImpl implements UserPendingReadPlatformService { + + private final JdbcTemplate jdbcTemplate; + private final UserPendingRequestRepositoryWrapper pendingRequestRepositoryWrapper; + private final Gson gson = GoogleGsonSerializerHelper.createSimpleGson(); + + private static final String SELECT_SQL = """ + SELECT r.id AS id, + r.user_id AS userId, + r.request_type AS requestType, + r.status AS status, + r.payload_json AS payloadJson, + r.original_json AS originalJson, + r.maker_id AS makerId, + maker.username AS makerUsername, + r.submitted_date AS submittedDate, + r.checker_id AS checkerId, + checker.username AS checkerUsername, + r.checked_date AS checkedDate, + r.reject_reason AS rejectReason + FROM m_user_pending_request r + JOIN m_appuser maker ON maker.id = r.maker_id + LEFT JOIN m_appuser checker ON checker.id = r.checker_id + """; + + @Override + @Transactional(readOnly = true) + public Page retrieveAllPendingRequests(final String status, final String username, + final String fromDate, final String toDate, final Integer offset, final Integer limit) { + final StringBuilder where = new StringBuilder(" WHERE 1=1"); + final List args = new ArrayList<>(); + + if (StringUtils.isNotBlank(status)) { + where.append(" AND r.status = ?"); + args.add(status.trim().toUpperCase()); + } else { + where.append(" AND r.status = 'PENDING'"); + } + + if (StringUtils.isNotBlank(fromDate)) { + where.append(" AND r.submitted_date::date >= ?::date"); + args.add(fromDate); + } + if (StringUtils.isNotBlank(toDate)) { + where.append(" AND r.submitted_date::date <= ?::date"); + args.add(toDate); + } + + final String countSql = "SELECT COUNT(*) FROM m_user_pending_request r " + where; + final Long totalFilteredRecords = this.jdbcTemplate.queryForObject(countSql, Long.class, args.toArray()); + + final int pageOffset = offset == null ? 0 : offset; + final int pageLimit = limit == null ? 20 : limit; + final String pageSql = SELECT_SQL + where + " ORDER BY r.submitted_date DESC LIMIT ? OFFSET ?"; + final List pageArgs = new ArrayList<>(args); + pageArgs.add(pageLimit); + pageArgs.add(pageOffset); + + final List items = this.jdbcTemplate.query(pageSql, new UserPendingRowMapper(), pageArgs.toArray()); + + return new Page<>(items, totalFilteredRecords == null ? 0 : totalFilteredRecords.intValue()); + } + + @Override + @Transactional(readOnly = true) + public UserPendingRequestData retrievePendingRequest(final Long id) { + final UserPendingRequest request = this.pendingRequestRepositoryWrapper.findOneWithNotFoundDetection(id); + final String sql = SELECT_SQL + " WHERE r.id = ?"; + return this.jdbcTemplate.queryForObject(sql, new UserPendingDetailRowMapper(), id); + } + + private static class UserPendingRowMapper implements RowMapper { + + @Override + public UserPendingRequestData mapRow(final ResultSet rs, final int rowNum) throws SQLException { + final Long id = rs.getLong("id"); + final Long userId = JdbcSupport.getLong(rs, "userId"); + final String requestType = rs.getString("requestType"); + final String status = rs.getString("status"); + final String payloadJson = rs.getString("payloadJson"); + final Long makerId = rs.getLong("makerId"); + final String makerUsername = rs.getString("makerUsername"); + final OffsetDateTime submittedDate = rs.getObject("submittedDate", OffsetDateTime.class); + final Long checkerId = JdbcSupport.getLong(rs, "checkerId"); + final String checkerUsername = rs.getString("checkerUsername"); + final OffsetDateTime checkedDate = rs.getObject("checkedDate", OffsetDateTime.class); + final String rejectReason = rs.getString("rejectReason"); + + String username = null; + String firstname = null; + String lastname = null; + String email = null; + String mobileNo = null; + Long officeId = null; + String otpDeliveryMethod = null; + + try { + if (payloadJson != null) { + final JsonObject payloadObj = JsonParser.parseString(payloadJson).getAsJsonObject(); + if (payloadObj.has("username")) { + username = payloadObj.get("username").getAsString(); + } + if (payloadObj.has("firstname")) { + firstname = payloadObj.get("firstname").getAsString(); + } + if (payloadObj.has("lastname")) { + lastname = payloadObj.get("lastname").getAsString(); + } + if (payloadObj.has("email")) { + email = payloadObj.get("email").getAsString(); + } + if (payloadObj.has("mobileNo")) { + mobileNo = payloadObj.get("mobileNo").getAsString(); + } + if (payloadObj.has("officeId")) { + officeId = payloadObj.get("officeId").getAsLong(); + } + if (payloadObj.has("otpDeliveryMethod")) { + otpDeliveryMethod = payloadObj.get("otpDeliveryMethod").getAsString(); + } + } + } catch (Exception ignored) { + } + + return UserPendingRequestData.builder() + .id(id) + .userId(userId) + .requestType(requestType) + .status(status) + .username(username) + .firstname(firstname) + .lastname(lastname) + .email(email) + .mobileNo(mobileNo) + .officeId(officeId) + .otpDeliveryMethod(otpDeliveryMethod) + .makerId(makerId) + .makerUsername(makerUsername) + .submittedDate(submittedDate) + .checkerId(checkerId) + .checkerUsername(checkerUsername) + .checkedDate(checkedDate) + .rejectReason(rejectReason) + .build(); + } + } + + private static class UserPendingDetailRowMapper implements RowMapper { + + @Override + public UserPendingRequestData mapRow(final ResultSet rs, final int rowNum) throws SQLException { + final Long id = rs.getLong("id"); + final Long userId = JdbcSupport.getLong(rs, "userId"); + final String requestType = rs.getString("requestType"); + final String status = rs.getString("status"); + final String payloadJson = rs.getString("payloadJson"); + final String originalJson = rs.getString("originalJson"); + final Long makerId = rs.getLong("makerId"); + final String makerUsername = rs.getString("makerUsername"); + final OffsetDateTime submittedDate = rs.getObject("submittedDate", OffsetDateTime.class); + final Long checkerId = JdbcSupport.getLong(rs, "checkerId"); + final String checkerUsername = rs.getString("checkerUsername"); + final OffsetDateTime checkedDate = rs.getObject("checkedDate", OffsetDateTime.class); + final String rejectReason = rs.getString("rejectReason"); + + final Map proposedValues = new LinkedHashMap<>(); + final Map originalValues = new LinkedHashMap<>(); + + String username = null; + String firstname = null; + String lastname = null; + String email = null; + String mobileNo = null; + Long officeId = null; + String otpDeliveryMethod = null; + Boolean passwordNeverExpires = null; + + try { + if (payloadJson != null) { + final JsonObject payloadObj = JsonParser.parseString(payloadJson).getAsJsonObject(); + for (final String key : payloadObj.keySet()) { + final JsonElement val = payloadObj.get(key); + if ("password".equals(key) || "repeatPassword".equals(key)) { + proposedValues.put("password", "Password will be set on approval"); + } else if (val.isJsonPrimitive()) { + if (val.getAsJsonPrimitive().isBoolean()) { + proposedValues.put(key, val.getAsBoolean()); + } else if (val.getAsJsonPrimitive().isNumber()) { + proposedValues.put(key, val.getAsNumber()); + } else { + proposedValues.put(key, val.getAsString()); + } + } else if (val.isJsonArray()) { + final List list = new ArrayList<>(); + for (final JsonElement elem : val.getAsJsonArray()) { + list.add(elem.getAsString()); + } + proposedValues.put(key, list); + } + } + + if (proposedValues.containsKey("username")) { + username = (String) proposedValues.get("username"); + } + if (proposedValues.containsKey("firstname")) { + firstname = (String) proposedValues.get("firstname"); + } + if (proposedValues.containsKey("lastname")) { + lastname = (String) proposedValues.get("lastname"); + } + if (proposedValues.containsKey("email")) { + email = (String) proposedValues.get("email"); + } + if (proposedValues.containsKey("mobileNo")) { + mobileNo = (String) proposedValues.get("mobileNo"); + } + if (proposedValues.get("officeId") instanceof Number n) { + officeId = n.longValue(); + } + if (proposedValues.containsKey("otpDeliveryMethod")) { + otpDeliveryMethod = (String) proposedValues.get("otpDeliveryMethod"); + } + if (proposedValues.get("passwordNeverExpires") instanceof Boolean b) { + passwordNeverExpires = b; + } + } + + if (originalJson != null) { + final JsonObject origObj = JsonParser.parseString(originalJson).getAsJsonObject(); + for (final String key : origObj.keySet()) { + final JsonElement val = origObj.get(key); + if (val.isJsonPrimitive()) { + if (val.getAsJsonPrimitive().isBoolean()) { + originalValues.put(key, val.getAsBoolean()); + } else if (val.getAsJsonPrimitive().isNumber()) { + originalValues.put(key, val.getAsNumber()); + } else { + originalValues.put(key, val.getAsString()); + } + } else if (val.isJsonArray()) { + final List list = new ArrayList<>(); + for (final JsonElement elem : val.getAsJsonArray()) { + list.add(elem.getAsString()); + } + originalValues.put(key, list); + } + } + if (proposedValues.containsKey("password")) { + originalValues.put("password", "[PROTECTED]"); + } + } + } catch (Exception ignored) { + } + + return UserPendingRequestData.builder() + .id(id) + .userId(userId) + .requestType(requestType) + .status(status) + .username(username) + .firstname(firstname) + .lastname(lastname) + .email(email) + .mobileNo(mobileNo) + .officeId(officeId) + .otpDeliveryMethod(otpDeliveryMethod) + .passwordNeverExpires(passwordNeverExpires) + .makerId(makerId) + .makerUsername(makerUsername) + .submittedDate(submittedDate) + .checkerId(checkerId) + .checkerUsername(checkerUsername) + .checkedDate(checkedDate) + .rejectReason(rejectReason) + .originalValues(originalValues) + .proposedValues(proposedValues) + .build(); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/useradministration/service/UserPendingWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/useradministration/service/UserPendingWritePlatformService.java new file mode 100644 index 000000000..736b23419 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/useradministration/service/UserPendingWritePlatformService.java @@ -0,0 +1,33 @@ +/** + * 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.useradministration.service; + +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; + +public interface UserPendingWritePlatformService { + + CommandProcessingResult submitCreateUser(JsonCommand command); + + CommandProcessingResult submitUpdateUser(Long userId, JsonCommand command); + + CommandProcessingResult approvePendingRequest(Long id); + + CommandProcessingResult rejectPendingRequest(Long id, JsonCommand command); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/useradministration/service/UserPendingWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/useradministration/service/UserPendingWritePlatformServiceImpl.java new file mode 100644 index 000000000..00c235802 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/useradministration/service/UserPendingWritePlatformServiceImpl.java @@ -0,0 +1,386 @@ +/** + * 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.useradministration.service; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; +import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.core.serialization.GoogleGsonSerializerHelper; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.organisation.office.domain.Office; +import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper; +import org.apache.fineract.organisation.staff.domain.Staff; +import org.apache.fineract.organisation.staff.domain.StaffRepositoryWrapper; +import org.apache.fineract.portfolio.client.domain.Client; +import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper; +import org.apache.fineract.useradministration.mfa.domain.MfaFactorType; +import org.apache.fineract.useradministration.domain.AppUser; +import org.apache.fineract.useradministration.domain.AppUserRepository; +import org.apache.fineract.useradministration.domain.Role; +import org.apache.fineract.useradministration.domain.RoleRepository; +import org.apache.fineract.useradministration.mfa.domain.UserMfaFactor; +import org.apache.fineract.useradministration.mfa.domain.UserMfaFactorRepository; +import org.apache.fineract.useradministration.domain.UserPendingRequest; +import org.apache.fineract.useradministration.domain.UserPendingRequestRepository; +import org.apache.fineract.useradministration.domain.UserPendingRequestRepositoryWrapper; +import org.apache.fineract.useradministration.domain.UserPendingRequestType; +import org.apache.fineract.useradministration.exception.RoleNotFoundException; +import org.apache.fineract.useradministration.exception.UserNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class UserPendingWritePlatformServiceImpl implements UserPendingWritePlatformService { + + private final PlatformSecurityContext context; + private final UserPendingRequestRepositoryWrapper pendingRequestRepositoryWrapper; + private final UserPendingRequestRepository pendingRequestRepository; + private final AppUserRepository appUserRepository; + private final OfficeRepositoryWrapper officeRepositoryWrapper; + private final RoleRepository roleRepository; + private final StaffRepositoryWrapper staffRepositoryWrapper; + private final ClientRepositoryWrapper clientRepositoryWrapper; + private final UserMfaFactorRepository userMfaFactorRepository; + private final ConfigurationDomainService configurationDomainService; + private final PasswordEncoder passwordEncoder; + private final UserDataValidator fromApiJsonDeserializer; + private final FromJsonHelper fromApiJsonHelper; + + @Override + @Transactional + public CommandProcessingResult submitCreateUser(final JsonCommand command) { + final AppUser maker = this.context.authenticatedUser(); + + this.fromApiJsonDeserializer.validateForCreate(command.json()); + + final String username = command.stringValueOfParameterNamed("username"); + final String email = command.stringValueOfParameterNamed("email"); + final String mobileNo = command.stringValueOfParameterNamed("mobileNo"); + + // Pre-flight checks on live users and pending creations + if (this.appUserRepository.findAppUserByName(username) != null) { + throw new PlatformDataIntegrityException("error.msg.user.duplicate.username", + "User with username " + username + " already exists.", "username", username); + } + + final String lockKey = "CREATE_USER:" + username.toLowerCase(); + if (this.pendingRequestRepository.existsByPendingLockKey(lockKey)) { + throw new GeneralPlatformDomainRuleException("error.msg.userpendingrequest.duplicate", + "A pending creation request for username '" + username + "' is already awaiting checker approval.", username); + } + + assertEmailAndMobileUnique(email, mobileNo, null); + + final JsonObject jsonObject = JsonParser.parseString(command.json()).getAsJsonObject(); + String passwordHash = null; + if (jsonObject.has("password") && !jsonObject.get("password").isJsonNull()) { + final String rawPassword = jsonObject.get("password").getAsString(); + if (!rawPassword.isBlank()) { + passwordHash = this.passwordEncoder.encode(rawPassword); + } + jsonObject.addProperty("password", "[PROTECTED]"); + if (jsonObject.has("repeatPassword")) { + jsonObject.addProperty("repeatPassword", "[PROTECTED]"); + } + } + + final String sanitizedPayload = jsonObject.toString(); + final UserPendingRequest request = UserPendingRequest.create(UserPendingRequestType.CREATE_USER, null, + sanitizedPayload, null, passwordHash, maker.getId(), lockKey); + + final UserPendingRequest saved = this.pendingRequestRepositoryWrapper.saveAndFlush(request); + + return new CommandProcessingResultBuilder() + .withEntityId(saved.getId()) + .setRollbackTransaction(true) + .build(); + } + + @Override + @Transactional + public CommandProcessingResult submitUpdateUser(final Long userId, final JsonCommand command) { + final AppUser maker = this.context.authenticatedUser(); + final AppUser userToUpdate = this.appUserRepository.findById(userId) + .orElseThrow(() -> new UserNotFoundException(userId)); + + this.fromApiJsonDeserializer.validateForUpdate(command.json(), maker); + + final String email = command.stringValueOfParameterNamed("email"); + final String mobileNo = command.stringValueOfParameterNamed("mobileNo"); + assertEmailAndMobileUnique(email, mobileNo, userId); + + final String lockKey = "UPDATE_USER:" + userId; + if (this.pendingRequestRepository.existsByPendingLockKey(lockKey)) { + throw new GeneralPlatformDomainRuleException("error.msg.userpendingrequest.duplicate", + "A pending update request for user ID " + userId + " is already awaiting checker approval.", userId); + } + + // Build point-in-time original JSON snapshot + final Map originalSnapshot = new LinkedHashMap<>(); + originalSnapshot.put("username", userToUpdate.getUsername()); + originalSnapshot.put("firstname", userToUpdate.getFirstname()); + originalSnapshot.put("lastname", userToUpdate.getLastname()); + originalSnapshot.put("email", userToUpdate.getEmail()); + originalSnapshot.put("mobileNo", userToUpdate.getMobileNo()); + originalSnapshot.put("officeId", userToUpdate.getOffice() != null ? userToUpdate.getOffice().getId() : null); + originalSnapshot.put("officeName", userToUpdate.getOffice() != null ? userToUpdate.getOffice().getName() : null); + originalSnapshot.put("staffId", userToUpdate.getStaff() != null ? userToUpdate.getStaff().getId() : null); + originalSnapshot.put("passwordNeverExpires", userToUpdate.getPasswordNeverExpires()); + originalSnapshot.put("isSelfServiceUser", userToUpdate.isSelfServiceUser()); + final Set roleNames = new HashSet<>(); + final Set roleIds = new HashSet<>(); + if (userToUpdate.getRoles() != null) { + for (final Role r : userToUpdate.getRoles()) { + roleNames.add(r.getName()); + roleIds.add(r.getId()); + } + } + originalSnapshot.put("roles", roleIds); + originalSnapshot.put("roleNames", roleNames); + final String originalJson = GoogleGsonSerializerHelper.createSimpleGson().toJson(originalSnapshot); + + final JsonObject jsonObject = JsonParser.parseString(command.json()).getAsJsonObject(); + String passwordHash = null; + if (jsonObject.has("password") && !jsonObject.get("password").isJsonNull()) { + final String rawPassword = jsonObject.get("password").getAsString(); + if (!rawPassword.isBlank()) { + passwordHash = this.passwordEncoder.encode(rawPassword); + } + jsonObject.addProperty("password", "[PROTECTED]"); + if (jsonObject.has("repeatPassword")) { + jsonObject.addProperty("repeatPassword", "[PROTECTED]"); + } + } + + final String sanitizedPayload = jsonObject.toString(); + final UserPendingRequest request = UserPendingRequest.create(UserPendingRequestType.UPDATE_USER, userId, + sanitizedPayload, originalJson, passwordHash, maker.getId(), lockKey); + + final UserPendingRequest saved = this.pendingRequestRepositoryWrapper.saveAndFlush(request); + + return new CommandProcessingResultBuilder() + .withEntityId(saved.getId()) + .setRollbackTransaction(true) + .build(); + } + + @Override + @Transactional + public CommandProcessingResult approvePendingRequest(final Long id) { + final AppUser checker = this.context.authenticatedUser(); + final UserPendingRequest request = this.pendingRequestRepositoryWrapper.findOneWithNotFoundDetection(id); + + if (!request.isPending()) { + throw new GeneralPlatformDomainRuleException("error.msg.userpendingrequest.not.pending", + "User pending request " + id + " has already been actioned.", id); + } + + if (request.isMadeBy(checker.getId())) { + throw new GeneralPlatformDomainRuleException("error.msg.userpendingrequest.self.approval", + "You cannot approve a user request you submitted yourself — a different user must action it.", id); + } + + final Long resourceId; + if (UserPendingRequestType.CREATE_USER.name().equals(request.getRequestType())) { + resourceId = finalizeCreateUser(request); + } else if (UserPendingRequestType.UPDATE_USER.name().equals(request.getRequestType())) { + resourceId = finalizeUpdateUser(request); + } else { + throw new GeneralPlatformDomainRuleException("error.msg.userpendingrequest.type.invalid", + "Unknown request type: " + request.getRequestType(), request.getRequestType()); + } + + request.approve(checker.getId()); + this.pendingRequestRepositoryWrapper.saveAndFlush(request); + + return new CommandProcessingResultBuilder() + .withEntityId(resourceId) + .withResourceIdAsString(resourceId.toString()) + .build(); + } + + @Override + @Transactional + public CommandProcessingResult rejectPendingRequest(final Long id, final JsonCommand command) { + final AppUser checker = this.context.authenticatedUser(); + final UserPendingRequest request = this.pendingRequestRepositoryWrapper.findOneWithNotFoundDetection(id); + + if (!request.isPending()) { + throw new GeneralPlatformDomainRuleException("error.msg.userpendingrequest.not.pending", + "User pending request " + id + " has already been actioned.", id); + } + + if (request.isMadeBy(checker.getId())) { + throw new GeneralPlatformDomainRuleException("error.msg.userpendingrequest.self.approval", + "You cannot reject a user request you submitted yourself — a different user must action it.", id); + } + + String reason = command != null ? command.stringValueOfParameterNamedAllowingNull("reason") : null; + if (reason == null && command != null) { + reason = command.stringValueOfParameterNamedAllowingNull("rejectReason"); + } + if (reason == null || reason.isBlank()) { + throw new GeneralPlatformDomainRuleException("error.msg.userpendingrequest.reject.reason.required", + "A reason is required to reject a user pending request.", id); + } + + request.reject(checker.getId(), reason); + this.pendingRequestRepositoryWrapper.saveAndFlush(request); + + return new CommandProcessingResultBuilder() + .withEntityId(request.getId()) + .withResourceIdAsString(request.getId().toString()) + .build(); + } + + private Long finalizeCreateUser(final UserPendingRequest request) { + final JsonElement parsedElement = this.fromApiJsonHelper.parse(request.getPayloadJson()); + final JsonCommand replay = JsonCommand.fromJsonElement(null, parsedElement, this.fromApiJsonHelper); + + final Long officeId = replay.longValueOfParameterNamed("officeId"); + final Office userOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(officeId); + + final String[] roles = replay.arrayValueOfParameterNamed("roles"); + final Set allRoles = assembleSetOfRoles(roles); + + final Long staffId = replay.longValueOfParameterNamed("staffId"); + final Staff linkedStaff = staffId != null + ? this.staffRepositoryWrapper.findByOfficeWithNotFoundDetection(staffId, userOffice.getId()) + : null; + + final Collection clients = null; + + final AppUser appUser = AppUser.fromJson(userOffice, linkedStaff, allRoles, clients, replay); + + if (this.configurationDomainService.isForcePasswordResetOnFirstLoginEnabled()) { + appUser.updatePasswordResetRequired(true); + } + + // Set pre-hashed password + if (request.getPasswordHash() != null) { + appUser.updatePassword(request.getPasswordHash()); + } + + this.appUserRepository.saveAndFlush(appUser); + + // MFA Enrollment + final String otpDeliveryMethod = replay.stringValueOfParameterNamed("otpDeliveryMethod"); + if (otpDeliveryMethod != null) { + final MfaFactorType factorType = MfaFactorType.fromString(otpDeliveryMethod); + final UserMfaFactor factor = new UserMfaFactor(); + factor.setAppUser(appUser); + factor.setFactorType(factorType); + factor.setDestination(factorType == MfaFactorType.SMS ? appUser.getMobileNo() : appUser.getEmail()); + factor.setVerified(true); + factor.setVerifiedOn(OffsetDateTime.now(ZoneOffset.UTC)); + this.userMfaFactorRepository.save(factor); + } + + return appUser.getId(); + } + + private Long finalizeUpdateUser(final UserPendingRequest request) { + final AppUser userToUpdate = this.appUserRepository.findById(request.getUserId()) + .orElseThrow(() -> new UserNotFoundException(request.getUserId())); + + final JsonElement parsedElement = this.fromApiJsonHelper.parse(request.getPayloadJson()); + final JsonCommand replay = JsonCommand.fromJsonElement(request.getUserId(), parsedElement, this.fromApiJsonHelper); + + final Map changes = userToUpdate.update(replay, null, null); + + if (changes.containsKey("officeId")) { + final Long officeId = (Long) changes.get("officeId"); + final Office office = this.officeRepositoryWrapper.findOneWithNotFoundDetection(officeId); + userToUpdate.changeOffice(office); + } + if (changes.containsKey("staffId")) { + final Long staffId = (Long) changes.get("staffId"); + Staff linkedStaff = null; + if (staffId != null) { + linkedStaff = this.staffRepositoryWrapper.findByOfficeWithNotFoundDetection(staffId, userToUpdate.getOffice().getId()); + } + userToUpdate.changeStaff(linkedStaff); + } + if (changes.containsKey("roles")) { + final String[] roleIds = (String[]) changes.get("roles"); + final Set allRoles = assembleSetOfRoles(roleIds); + userToUpdate.updateRoles(allRoles); + } + if (request.getPasswordHash() != null) { + userToUpdate.updatePassword(request.getPasswordHash()); + } + + this.appUserRepository.saveAndFlush(userToUpdate); + return userToUpdate.getId(); + } + + private Set assembleSetOfRoles(final String[] rolesArray) { + final Set allRoles = new HashSet<>(); + if (!ObjectUtils.isEmpty(rolesArray)) { + for (final String roleId : rolesArray) { + final Long id = Long.valueOf(roleId); + final Role role = this.roleRepository.findById(id).orElseThrow(() -> new RoleNotFoundException(id)); + allRoles.add(role); + } + } + return allRoles; + } + + private void assertEmailAndMobileUnique(final String email, final String mobileNo, final Long excludeUserId) { + if (email != null && !email.isBlank()) { + final java.util.Optional emailOwner = excludeUserId == null + ? this.appUserRepository.findByEmailIgnoreCase(email) + : this.appUserRepository.findByEmailIgnoreCaseAndIdNot(email, excludeUserId); + if (emailOwner.isPresent()) { + throw new PlatformDataIntegrityException("error.msg.user.duplicate.email", + "User with email " + email + " already exists.", "email", email); + } + } + if (mobileNo != null && !mobileNo.isBlank()) { + final java.util.Optional mobileOwner = excludeUserId == null + ? this.appUserRepository.findByMobileNo(mobileNo) + : this.appUserRepository.findByMobileNoAndIdNot(mobileNo, excludeUserId); + if (mobileOwner.isPresent()) { + throw new PlatformDataIntegrityException("error.msg.user.duplicate.mobile", + "User with mobile number " + mobileNo + " already exists.", "mobileNo", mobileNo); + } + } + } +} 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 4b383b1b2..472524953 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 @@ -578,4 +578,12 @@ + + + + + + + + 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/main/resources/db/changelog/tenant/parts/0475_enable_admin_user_maker_checker.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0475_enable_admin_user_maker_checker.xml new file mode 100644 index 000000000..9ddde27a5 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0475_enable_admin_user_maker_checker.xml @@ -0,0 +1,77 @@ + + + + + ANY + + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'authorisation', 'PERMISSIONS_ROLE_CHECKER', 'ROLE', 'PERMISSIONS_CHECKER', FALSE + WHERE NOT EXISTS ( + SELECT 1 FROM m_permission WHERE code = 'PERMISSIONS_ROLE_CHECKER' + ); + + + + + ANY + + + SELECT COUNT(*) + FROM m_permission + WHERE code IN ( + 'CREATE_USER', 'UPDATE_USER', 'DELETE_USER', + 'CREATE_ROLE', 'UPDATE_ROLE', 'PERMISSIONS_ROLE', + 'DELETE_ROLE', 'ENABLE_ROLE', 'DISABLE_ROLE' + ) + + + + UPDATE m_permission + SET can_maker_checker = TRUE + WHERE code IN ( + 'CREATE_USER', 'UPDATE_USER', 'DELETE_USER', + 'CREATE_ROLE', 'UPDATE_ROLE', 'PERMISSIONS_ROLE', + 'DELETE_ROLE', 'ENABLE_ROLE', 'DISABLE_ROLE' + ); + + + + + 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_USER_CHECKER', 'UPDATE_USER_CHECKER', 'DELETE_USER_CHECKER', + 'CREATE_ROLE_CHECKER', 'UPDATE_ROLE_CHECKER', 'PERMISSIONS_ROLE_CHECKER', + 'DELETE_ROLE_CHECKER', 'ENABLE_ROLE_CHECKER', 'DISABLE_ROLE_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_USER_CHECKER', 'UPDATE_USER_CHECKER', 'DELETE_USER_CHECKER', + 'CREATE_ROLE_CHECKER', 'UPDATE_ROLE_CHECKER', 'PERMISSIONS_ROLE_CHECKER', + 'DELETE_ROLE_CHECKER', 'ENABLE_ROLE_CHECKER', 'DISABLE_ROLE_CHECKER' + ) + AND NOT EXISTS ( + SELECT 1 FROM m_role_permission rp + WHERE rp.role_id = ur.role_id AND rp.permission_id = p.id + ); + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0476_add_user_pending_request_and_enable_charge_mc.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0476_add_user_pending_request_and_enable_charge_mc.xml new file mode 100644 index 000000000..9d0c6db5a --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0476_add_user_pending_request_and_enable_charge_mc.xml @@ -0,0 +1,98 @@ + + + + + ANY + + CREATE TABLE IF NOT EXISTS m_user_pending_request ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NULL REFERENCES m_appuser(id) ON DELETE SET NULL, + request_type VARCHAR(20) NOT NULL, + status VARCHAR(20) NOT NULL, + payload_json TEXT NOT NULL, + original_json TEXT NULL, + password_hash VARCHAR(255) NULL, + maker_id BIGINT NOT NULL REFERENCES m_appuser(id), + submitted_date TIMESTAMPTZ NOT NULL, + checker_id BIGINT NULL REFERENCES m_appuser(id), + checked_date TIMESTAMPTZ NULL, + reject_reason VARCHAR(500) NULL, + pending_lock_key VARCHAR(100) NULL UNIQUE + ); + + CREATE INDEX IF NOT EXISTS idx_user_pending_req_status ON m_user_pending_request(status); + CREATE INDEX IF NOT EXISTS idx_user_pending_req_user_id ON m_user_pending_request(user_id); + CREATE INDEX IF NOT EXISTS idx_user_pending_req_maker ON m_user_pending_request(maker_id); + + + + + ANY + + -- Disable native MC on CREATE_USER and UPDATE_USER to avoid core password sanitization error + UPDATE m_permission + SET can_maker_checker = FALSE + WHERE code IN ('CREATE_USER', 'UPDATE_USER'); + + -- Enable native MC on UPDATE_CHARGE and DELETE_CHARGE + UPDATE m_permission + SET can_maker_checker = TRUE + WHERE code IN ('UPDATE_CHARGE', 'DELETE_CHARGE'); + + -- Insert bespoke user maker-checker permissions + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'user_administration', 'SUBMIT_CREATE_USER', 'USER', 'SUBMIT_CREATE', FALSE + WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'SUBMIT_CREATE_USER'); + + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'user_administration', 'SUBMIT_UPDATE_USER', 'USER', 'SUBMIT_UPDATE', FALSE + WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'SUBMIT_UPDATE_USER'); + + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'user_administration', 'APPROVE_USER', 'USER', 'APPROVE', FALSE + WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'APPROVE_USER'); + + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'user_administration', 'REJECT_USER', 'USER', 'REJECT', FALSE + WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'REJECT_USER'); + + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'user_administration', 'READ_USER_PENDING_REQUEST', 'USER', 'READ_PENDING', FALSE + WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_USER_PENDING_REQUEST'); + + -- Grant permissions to Super user + 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 ( + 'UPDATE_CHARGE', 'UPDATE_CHARGE_CHECKER', 'DELETE_CHARGE', 'DELETE_CHARGE_CHECKER', + 'SUBMIT_CREATE_USER', 'SUBMIT_UPDATE_USER', 'APPROVE_USER', 'REJECT_USER', 'READ_USER_PENDING_REQUEST' + ) + AND NOT EXISTS ( + SELECT 1 FROM m_role_permission rp + WHERE rp.role_id = r.id AND rp.permission_id = p.id + ); + + -- Grant permissions to bankadmin + 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 ( + 'UPDATE_CHARGE', 'UPDATE_CHARGE_CHECKER', 'DELETE_CHARGE', 'DELETE_CHARGE_CHECKER', + 'SUBMIT_CREATE_USER', 'SUBMIT_UPDATE_USER', 'APPROVE_USER', 'REJECT_USER', 'READ_USER_PENDING_REQUEST' + ) + AND NOT EXISTS ( + SELECT 1 FROM m_role_permission rp + WHERE rp.role_id = ur.role_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..c4db9dadf 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,528 @@ 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_FixedDepositProduct_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_FIXEDDEPOSITPRODUCT"); + Mockito.when(commandSource.getEntityName()).thenReturn("FIXEDDEPOSITPRODUCT"); + Mockito.when(commandSource.getActionName()).thenReturn("CREATE"); + Mockito.when(commandSource.getId()).thenReturn(114L); + Mockito.when(commandSource.getResourceId()).thenReturn(214L); + Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("CREATE_FIXEDDEPOSITPRODUCT")).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_FixedDepositProduct_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_FIXEDDEPOSITPRODUCT"); + Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("CREATE_FIXEDDEPOSITPRODUCT")).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_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(); + } + + @Test + public void testProcessCommand_UserOperations_SuperUserBypassBlocked_ThrowsRollbackException() { + String[] actions = { "CREATE", "UPDATE", "DELETE" }; + String[] permissions = { "CREATE_USER", "UPDATE_USER", "DELETE_USER" }; + + for (int i = 0; i < actions.length; i++) { + 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.getEntityName()).thenReturn("USER"); + Mockito.when(commandSource.getActionName()).thenReturn(actions[i]); + Mockito.when(commandSource.getPermissionCode()).thenReturn(permissions[i]); + Mockito.when(commandSource.isSanitized()).thenReturn(false); + Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask(permissions[i])).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_RoleOperations_SuperUserBypassBlocked_ThrowsRollbackException() { + String[] actions = { "CREATE", "UPDATE", "PERMISSIONS", "DELETE" }; + String[] permissions = { "CREATE_ROLE", "UPDATE_ROLE", "PERMISSIONS_ROLE", "DELETE_ROLE" }; + + for (int i = 0; i < actions.length; i++) { + 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.getEntityName()).thenReturn("ROLE"); + Mockito.when(commandSource.getActionName()).thenReturn(actions[i]); + Mockito.when(commandSource.getPermissionCode()).thenReturn(permissions[i]); + Mockito.when(commandSource.isSanitized()).thenReturn(false); + Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask(permissions[i])).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_UserAndRole_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_USER"); + Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("CREATE_USER")).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); + } +}