Merge branch 'maker-checker' into dev
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s

Brings in maker-checker for GL accounts, charges, product creation (loan,
savings, current, share, FD), admin users/roles and base interest rates, plus
the UserPendingRequest domain and AppUserPreference.

Two resolutions:

changelog-tenant.xml — both branches appended <include> lines at the end of the
file, so the conflict was append-vs-append with no semantic overlap. Kept both
sets in order: dev's 0470-0499 followed by maker-checker's eight. Liquibase runs
in <include> order, not filename order, so the reused 0466-0476 numbers change
nothing (dev already carries duplicate numbers in that range).

application.properties — the branch carried another developer's local database
password, which would have stopped this backend connecting at startup. Restored
ours; the file is now byte-identical to its pre-merge state.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-01 18:07:24 +05:30
co-authored by Claude Opus 5
50 changed files with 3470 additions and 8 deletions
@@ -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<String> sanitizeKeys) {
if (sanitizeKeys == null || sanitizeKeys.isEmpty()) {
return;
@@ -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";
@@ -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
@@ -61,6 +61,11 @@ public class ConfigurationDomainServiceJpa implements ConfigurationDomainService
return false;
}
@Override
public boolean isMakerCheckerEnabled() {
return getGlobalConfigurationPropertyData(GlobalConfigurationConstants.MAKER_CHECKER).isEnabled();
}
@Override
public List<String> getAllowedLoanStatusesForExternalAssetTransfer() {
final GlobalConfigurationPropertyData property = getGlobalConfigurationPropertyData(
@@ -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<LocalDate>) (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);
}
}
@@ -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);
}
}
@@ -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<FdBaseInterestRateEffective> activeVersions = effectiveRepository
@@ -85,6 +93,125 @@ public class FdBaseInterestRateReadPlatformServiceImpl implements FdBaseInterest
return toData(newVersion);
}
private void validateRequest(final FdInterestRateSaveRequest request) {
final List<ApiParameterError> 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<FdInterestRateSaveRequest.SlabRequest> slabs) {
final List<ApiParameterError> 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<FdInterestRateSaveRequest.SlabRequest> 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<ApiParameterError> dataValidationErrors) {
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
}
private FdInterestRateEffectiveData toData(FdBaseInterestRateEffective e) {
List<FdInterestRateSlabData> slabs = slabRepository.findByEffectiveIdOrderBySrNo(e.getId()).stream()
.map(s -> FdInterestRateSlabData.builder()
@@ -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);
}
@@ -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();
}
}
@@ -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,
@@ -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);
}
}
@@ -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<org.apache.fineract.useradministration.data.UserPendingRequestData> 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<org.apache.fineract.useradministration.data.UserPendingRequestData> 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.")
@@ -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;
}
@@ -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<String> 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<String, Object> originalValues;
private final Map<String, Object> proposedValues;
}
@@ -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<Long> {
@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;
}
}
@@ -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<AppUserPreference, Long> {
Optional<AppUserPreference> findByAppUserIdAndName(Long appUserId, String name);
}
@@ -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<Long> {
@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;
}
}
@@ -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<UserPendingRequest, Long>, JpaSpecificationExecutor<UserPendingRequest> {
Optional<UserPendingRequest> findByPendingLockKey(String pendingLockKey);
boolean existsByPendingLockKey(String pendingLockKey);
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -0,0 +1,5 @@
package org.apache.fineract.useradministration.service;
public interface AppUserPreferenceReadPlatformService {
String getPreferenceValue(Long appUserId, String name);
}
@@ -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<AppUserPreference> pref = appUserPreferenceRepository.findByAppUserIdAndName(appUserId, name);
return pref.map(AppUserPreference::getValue).orElse(null);
}
}
@@ -0,0 +1,5 @@
package org.apache.fineract.useradministration.service;
public interface AppUserPreferenceWritePlatformService {
void updatePreference(Long appUserId, String name, String value);
}
@@ -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);
}
}
@@ -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<UserPendingRequestData> retrieveAllPendingRequests(String status, String username, String fromDate, String toDate,
Integer offset, Integer limit);
UserPendingRequestData retrievePendingRequest(Long id);
}
@@ -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<UserPendingRequestData> 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<Object> 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<Object> pageArgs = new ArrayList<>(args);
pageArgs.add(pageLimit);
pageArgs.add(pageOffset);
final List<UserPendingRequestData> 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<UserPendingRequestData> {
@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<UserPendingRequestData> {
@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<String, Object> proposedValues = new LinkedHashMap<>();
final Map<String, Object> 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<String> 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<String> 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();
}
}
}
@@ -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);
}
@@ -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<String, Object> 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<String> roleNames = new HashSet<>();
final Set<Long> 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<Role> allRoles = assembleSetOfRoles(roles);
final Long staffId = replay.longValueOfParameterNamed("staffId");
final Staff linkedStaff = staffId != null
? this.staffRepositoryWrapper.findByOfficeWithNotFoundDetection(staffId, userOffice.getId())
: null;
final Collection<Client> 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<String, Object> 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<Role> allRoles = assembleSetOfRoles(roleIds);
userToUpdate.updateRoles(allRoles);
}
if (request.getPasswordHash() != null) {
userToUpdate.updatePassword(request.getPasswordHash());
}
this.appUserRepository.saveAndFlush(userToUpdate);
return userToUpdate.getId();
}
private Set<Role> assembleSetOfRoles(final String[] rolesArray) {
final Set<Role> 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<AppUser> 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<AppUser> 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);
}
}
}
}
@@ -578,4 +578,12 @@
<include file="parts/0496_create_voucher_line.xml" relativeToChangelogFile="true"/>
<include file="parts/0497_voucher_reference_key.xml" relativeToChangelogFile="true"/>
<include file="parts/0498_add_rate_percentage_to_dividend_payout.xml" relativeToChangelogFile="true"/>
<include file="parts/0466_create_m_appuser_preference.xml" relativeToChangelogFile="true"/>
<include file="parts/0467_enable_glaccount_maker_checker.xml" relativeToChangelogFile="true"/>
<include file="parts/0470_enable_cbs_base_interest_rate_maker_checker.xml" relativeToChangelogFile="true"/>
<include file="parts/0471_enable_charge_maker_checker.xml" relativeToChangelogFile="true"/>
<include file="parts/0472_enable_product_creation_maker_checker.xml" relativeToChangelogFile="true"/>
<include file="parts/0474_enable_fixed_deposit_product_maker_checker.xml" relativeToChangelogFile="true"/>
<include file="parts/0475_enable_admin_user_maker_checker.xml" relativeToChangelogFile="true"/>
<include file="parts/0476_add_user_pending_request_and_enable_charge_mc.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
<changeSet id="0466_create_m_appuser_preference" author="fineract">
<preConditions onFail="MARK_RAN"><not><tableExists tableName="m_appuser_preference"/></not></preConditions>
<createTable tableName="m_appuser_preference">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="appuser_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="name" type="VARCHAR(100)">
<constraints nullable="false"/>
</column>
<column name="value" type="TEXT">
<constraints nullable="false"/>
</column>
</createTable>
<addForeignKeyConstraint baseTableName="m_appuser_preference" baseColumnNames="appuser_id" constraintName="fk_m_appuser_preference_appuser_id" referencedTableName="m_appuser" referencedColumnNames="id"/>
<addUniqueConstraint tableName="m_appuser_preference" columnNames="appuser_id, name" constraintName="uq_appuser_preference_name"/>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet id="0467_enable_glaccount_maker_checker" author="fineract" runOnChange="false">
<validCheckSum>ANY</validCheckSum>
<preConditions onFail="HALT">
<sqlCheck expectedResult="3">
SELECT COUNT(*)
FROM m_permission
WHERE entity_name = 'GLACCOUNT'
AND action_name IN ('CREATE', 'UPDATE', 'DELETE')
</sqlCheck>
</preConditions>
<sql>
UPDATE m_permission
SET can_maker_checker = TRUE
WHERE entity_name = 'GLACCOUNT'
AND action_name IN ('CREATE', 'UPDATE', 'DELETE');
</sql>
</changeSet>
</databaseChangeLog>
@@ -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. -->
<changeSet id="0468_add_address_id_to_welcome_kit_dispatch" author="CBS" runOnChange="false">
<validCheckSum>ANY</validCheckSum>
<preConditions onFail="MARK_RAN">
<not>
<columnExists tableName="m_welcome_kit_dispatch" columnName="address_id"/>
@@ -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. -->
<changeSet id="0469_create_database_export_request" author="CBS" runOnChange="false">
<validCheckSum>ANY</validCheckSum>
<createTable tableName="m_database_export_request">
<column name="id" type="BIGSERIAL">
<constraints primaryKey="true" nullable="false"/>
@@ -50,6 +51,7 @@
<!-- Requesting an export and approving one are deliberately separate permissions: the whole control
depends on no single user holding both. -->
<changeSet id="0469_database_export_permissions" author="CBS" runOnChange="false">
<validCheckSum>ANY</validCheckSum>
<sql>
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'authorisation', 'CREATE_DATABASEEXPORT', 'DATABASEEXPORT', 'CREATE', false
@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet id="0470_cbs_base_interest_rate_maker_checker_permissions" author="fineract" runOnChange="false">
<validCheckSum>ANY</validCheckSum>
<sql>
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';
</sql>
</changeSet>
<changeSet id="0470_grant_cbs_base_interest_rate_permissions_to_roles" author="fineract" runOnChange="false">
<validCheckSum>ANY</validCheckSum>
<sql>
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
);
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet id="0471_enable_charge_maker_checker" author="fineract" runOnChange="false">
<validCheckSum>ANY</validCheckSum>
<preConditions onFail="HALT">
<sqlCheck expectedResult="1">
SELECT COUNT(*)
FROM m_permission
WHERE entity_name = 'CHARGE'
AND action_name = 'CREATE'
</sqlCheck>
</preConditions>
<sql>
UPDATE m_permission
SET can_maker_checker = TRUE
WHERE entity_name = 'CHARGE'
AND action_name = 'CREATE';
</sql>
</changeSet>
<changeSet id="0471_grant_charge_maker_checker_permissions_to_roles" author="fineract" runOnChange="false">
<validCheckSum>ANY</validCheckSum>
<sql>
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
);
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet id="0472_add_shareproduct_checker_permission_if_not_exists" author="fineract" runOnChange="false">
<validCheckSum>ANY</validCheckSum>
<sql>
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'
);
</sql>
</changeSet>
<changeSet id="0472_enable_product_creation_maker_checker" author="fineract" runOnChange="false">
<validCheckSum>ANY</validCheckSum>
<preConditions onFail="HALT">
<sqlCheck expectedResult="4">
SELECT COUNT(*)
FROM m_permission
WHERE code IN ('CREATE_LOANPRODUCT', 'CREATE_SAVINGSPRODUCT', 'CREATE_CURRENTPRODUCT', 'CREATE_SHAREPRODUCT')
</sqlCheck>
</preConditions>
<sql>
UPDATE m_permission
SET can_maker_checker = TRUE
WHERE code IN ('CREATE_LOANPRODUCT', 'CREATE_SAVINGSPRODUCT', 'CREATE_CURRENTPRODUCT', 'CREATE_SHAREPRODUCT');
</sql>
</changeSet>
<changeSet id="0472_grant_product_creation_maker_checker_permissions_to_roles" author="fineract" runOnChange="false">
<validCheckSum>ANY</validCheckSum>
<sql>
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
);
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet id="0474_add_fixeddepositproduct_checker_permission_if_not_exists" author="fineract" runOnChange="false">
<validCheckSum>ANY</validCheckSum>
<sql>
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'
);
</sql>
</changeSet>
<changeSet id="0474_enable_fixed_deposit_product_creation_maker_checker" author="fineract" runOnChange="false">
<validCheckSum>ANY</validCheckSum>
<preConditions onFail="HALT">
<sqlCheck expectedResult="1">
SELECT COUNT(*)
FROM m_permission
WHERE code = 'CREATE_FIXEDDEPOSITPRODUCT'
</sqlCheck>
</preConditions>
<sql>
UPDATE m_permission
SET can_maker_checker = TRUE
WHERE code = 'CREATE_FIXEDDEPOSITPRODUCT';
</sql>
</changeSet>
<changeSet id="0474_grant_fixed_deposit_product_creation_maker_checker_permissions_to_roles" author="fineract" runOnChange="false">
<validCheckSum>ANY</validCheckSum>
<sql>
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
);
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet id="0475_add_permissions_role_checker_if_not_exists" author="fineract" runOnChange="false">
<validCheckSum>ANY</validCheckSum>
<sql>
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'
);
</sql>
</changeSet>
<changeSet id="0475_enable_admin_user_and_role_maker_checker" author="fineract" runOnChange="false">
<validCheckSum>ANY</validCheckSum>
<preConditions onFail="HALT">
<sqlCheck expectedResult="9">
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'
)
</sqlCheck>
</preConditions>
<sql>
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'
);
</sql>
</changeSet>
<changeSet id="0475_grant_admin_user_and_role_checker_permissions_to_roles" author="fineract" runOnChange="false">
<validCheckSum>ANY</validCheckSum>
<sql>
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
);
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet id="0476_create_m_user_pending_request_table" author="fineract" runOnChange="false">
<validCheckSum>ANY</validCheckSum>
<sql>
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);
</sql>
</changeSet>
<changeSet id="0476_configure_user_and_charge_maker_checker_permissions" author="fineract" runOnChange="false">
<validCheckSum>ANY</validCheckSum>
<sql>
-- 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
);
</sql>
</changeSet>
</databaseChangeLog>
@@ -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();
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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<FdInterestRateSaveRequest.SlabRequest> 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<FdInterestRateSaveRequest.SlabRequest> 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<FdInterestRateSaveRequest.SlabRequest> 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<FdInterestRateSaveRequest.SlabRequest> 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<FdInterestRateSaveRequest.SlabRequest> 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<FdInterestRateSaveRequest.SlabRequest> 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<FdInterestRateSaveRequest.SlabRequest> 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<FdInterestRateSaveRequest.SlabRequest> 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<FdInterestRateSaveRequest.SlabRequest> 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<FdInterestRateSaveRequest.SlabRequest> 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<FdInterestRateSaveRequest.SlabRequest> 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());
}
}
@@ -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));
}
}
@@ -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);
}
}