feat: add command handlers, permissions, and API resources for surveys and loan COB catch-up
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 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 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 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

This commit is contained in:
2026-09-28 12:55:21 +05:30
parent d8f6c25875
commit 6ee3bbe67c
12 changed files with 545 additions and 27 deletions
@@ -4809,4 +4809,36 @@ public class CommandWrapperBuilder {
this.href = "/loans/" + loanId + "/originators/" + originatorId;
return this;
}
public CommandWrapperBuilder createSurvey() {
this.actionName = "CREATE";
this.entityName = "SURVEY";
this.entityId = null;
this.href = "/surveys";
return this;
}
public CommandWrapperBuilder updateSurvey(final Long surveyId) {
this.actionName = "UPDATE";
this.entityName = "SURVEY";
this.entityId = surveyId;
this.href = "/surveys/" + surveyId;
return this;
}
public CommandWrapperBuilder activateSurvey(final Long surveyId) {
this.actionName = "ACTIVATE";
this.entityName = "SURVEY";
this.entityId = surveyId;
this.href = "/surveys/" + surveyId;
return this;
}
public CommandWrapperBuilder deactivateSurvey(final Long surveyId) {
this.actionName = "DEACTIVATE";
this.entityName = "SURVEY";
this.entityId = surveyId;
this.href = "/surveys/" + surveyId;
return this;
}
}
@@ -35,6 +35,7 @@ import org.apache.fineract.cob.data.OldestCOBProcessedLoanDTO;
import org.apache.fineract.cob.service.LoanCOBCatchUpService;
import org.apache.fineract.infrastructure.core.exception.JobIsNotFoundOrNotEnabledException;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.springframework.stereotype.Component;
@Path("/v1/loans")
@@ -44,6 +45,7 @@ import org.springframework.stereotype.Component;
public class LoanCOBCatchUpApiResource {
private final Optional<LoanCOBCatchUpService> loanCOBCatchUpServiceOp;
private final PlatformSecurityContext context;
@GET
@Path("oldest-cob-closed")
@@ -64,6 +66,7 @@ public class LoanCOBCatchUpApiResource {
@ApiResponse(responseCode = "202", description = "Catch Up has been started")
@ApiResponse(responseCode = "400", description = "Catch Up is already running")
public Response executeLoanCOBCatchUp() {
this.context.authenticatedUser().validateHasPermissionTo("EXECUTE_LOANCOBCATCHUP");
return loanCOBCatchUpServiceOp.map(loanCOBCatchUpService -> {
if (loanCOBCatchUpService.isCatchUpRunning().isCatchUpRunning()) {
return Response.status(Response.Status.BAD_REQUEST).build();
@@ -18,7 +18,6 @@
*/
package org.apache.fineract.spm.api;
import com.google.gson.Gson;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
@@ -37,10 +36,14 @@ import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.util.ArrayList;
import java.util.HashMap;
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.core.exception.UnrecognizedQueryParamException;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.spm.data.SurveyData;
import org.apache.fineract.spm.domain.Survey;
@@ -57,6 +60,8 @@ public class SpmApiResource {
private final PlatformSecurityContext securityContext;
private final SpmService spmService;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
private final DefaultToApiJsonSerializer<CommandProcessingResult> toApiJsonSerializer;
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@@ -103,12 +108,10 @@ public class SpmApiResource {
@Operation(summary = "Create a Survey", description = "Adds a new survey to collect client related data.\n" + "\n"
+ "Mandatory Fields\n" + "\n" + "countryCode, key, name, questions, responses, sequenceNo, text, description")
@ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") })
public String createSurvey(@Parameter(description = "Create survey") final SurveyData surveyData) {
this.securityContext.authenticatedUser();
final Survey survey = SurveyMapper.map(surveyData, new Survey());
this.spmService.createSurvey(survey);
return getResponse(survey.getId());
public String createSurvey(@Parameter(description = "Create survey") final String apiRequestBodyAsJson) {
final CommandWrapper commandRequest = new CommandWrapperBuilder().createSurvey().withJson(apiRequestBodyAsJson).build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.toApiJsonSerializer.serialize(result);
}
@PUT
@@ -116,12 +119,10 @@ public class SpmApiResource {
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Transactional
public String editSurvey(@PathParam("id") final Long id, final SurveyData surveyData) {
this.securityContext.authenticatedUser();
final Survey surveyToUpdate = this.spmService.findById(id);
final Survey survey = SurveyMapper.map(surveyData, surveyToUpdate);
this.spmService.updateSurvey(survey);
return getResponse(survey.getId());
public String editSurvey(@PathParam("id") final Long id, final String apiRequestBodyAsJson) {
final CommandWrapper commandRequest = new CommandWrapperBuilder().updateSurvey(id).withJson(apiRequestBodyAsJson).build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.toApiJsonSerializer.serialize(result);
}
@POST
@@ -131,22 +132,17 @@ public class SpmApiResource {
@Transactional
@Operation(summary = "Deactivate Survey", description = "")
@ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") })
public void activateOrDeactivateSurvey(@PathParam("id") final Long id, @QueryParam("command") final String command) {
this.securityContext.authenticatedUser();
public String activateOrDeactivateSurvey(@PathParam("id") final Long id, @QueryParam("command") final String command) {
final CommandWrapperBuilder builder = new CommandWrapperBuilder();
final CommandWrapper commandRequest;
if (command != null && command.equalsIgnoreCase("activate")) {
this.spmService.activateSurvey(id);
commandRequest = builder.activateSurvey(id).build();
} else if (command != null && command.equalsIgnoreCase("deactivate")) {
this.spmService.deactivateSurvey(id);
commandRequest = builder.deactivateSurvey(id).build();
} else {
throw new UnrecognizedQueryParamException("command", command);
}
}
private String getResponse(Long id) {
Gson gson = new Gson();
HashMap<String, Object> response = new HashMap<>();
response.put("resourceId", id);
return gson.toJson(response);
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.toApiJsonSerializer.serialize(result);
}
}
@@ -0,0 +1,53 @@
/**
* 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.spm.handler;
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.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.spm.service.SpmService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "SURVEY", action = "ACTIVATE")
public class ActivateSurveyCommandHandler implements NewCommandSourceHandler {
private final SpmService spmService;
@Autowired
public ActivateSurveyCommandHandler(final SpmService spmService) {
this.spmService = spmService;
}
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
this.spmService.activateSurvey(command.entityId());
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(command.entityId()) //
.build();
}
}
@@ -0,0 +1,71 @@
/**
* 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.spm.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.core.JsonProcessingException;
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.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.spm.data.SurveyData;
import org.apache.fineract.spm.domain.Survey;
import org.apache.fineract.spm.service.SpmService;
import org.apache.fineract.spm.util.SurveyMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "SURVEY", action = "CREATE")
public class CreateSurveyCommandHandler implements NewCommandSourceHandler {
private final SpmService spmService;
private final ObjectMapper objectMapper;
@Autowired
public CreateSurveyCommandHandler(final SpmService spmService, final ObjectMapper objectMapper) {
this.spmService = spmService;
this.objectMapper = objectMapper;
}
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
final SurveyData surveyData = readSurveyData(command);
final Survey survey = SurveyMapper.map(surveyData, new Survey());
this.spmService.createSurvey(survey);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(survey.getId()) //
.build();
}
private SurveyData readSurveyData(final JsonCommand command) {
try {
return this.objectMapper.readValue(command.json(), SurveyData.class);
} catch (final JsonProcessingException e) {
throw new PlatformDataIntegrityException("error.msg.survey.invalid.json", "Invalid survey payload: " + e.getMessage());
}
}
}
@@ -0,0 +1,53 @@
/**
* 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.spm.handler;
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.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.spm.service.SpmService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "SURVEY", action = "DEACTIVATE")
public class DeactivateSurveyCommandHandler implements NewCommandSourceHandler {
private final SpmService spmService;
@Autowired
public DeactivateSurveyCommandHandler(final SpmService spmService) {
this.spmService = spmService;
}
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
this.spmService.deactivateSurvey(command.entityId());
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(command.entityId()) //
.build();
}
}
@@ -0,0 +1,72 @@
/**
* 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.spm.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.core.JsonProcessingException;
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.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.spm.data.SurveyData;
import org.apache.fineract.spm.domain.Survey;
import org.apache.fineract.spm.service.SpmService;
import org.apache.fineract.spm.util.SurveyMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "SURVEY", action = "UPDATE")
public class UpdateSurveyCommandHandler implements NewCommandSourceHandler {
private final SpmService spmService;
private final ObjectMapper objectMapper;
@Autowired
public UpdateSurveyCommandHandler(final SpmService spmService, final ObjectMapper objectMapper) {
this.spmService = spmService;
this.objectMapper = objectMapper;
}
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
final SurveyData surveyData = readSurveyData(command);
final Survey surveyToUpdate = this.spmService.findById(command.entityId());
final Survey survey = SurveyMapper.map(surveyData, surveyToUpdate);
this.spmService.updateSurvey(survey);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(survey.getId()) //
.build();
}
private SurveyData readSurveyData(final JsonCommand command) {
try {
return this.objectMapper.readValue(command.json(), SurveyData.class);
} catch (final JsonProcessingException e) {
throw new PlatformDataIntegrityException("error.msg.survey.invalid.json", "Invalid survey payload: " + e.getMessage());
}
}
}
@@ -114,17 +114,33 @@ public class PermissionReadPlatformServiceImpl implements PermissionReadPlatform
+ "'SUBMITEDIT_CURRENTACCOUNT','APPROVEEDIT_CURRENTACCOUNT','REJECTEDIT_CURRENTACCOUNT',"
+ "'SUBMITEDIT_CLIENT','APPROVEEDIT_CLIENT','REJECTEDIT_CLIENT'";
/*
* Unlike DEAD_MAKER_CHECKER_CODES above, these codes ARE read by the live maker-checker pipeline
* (CommandSourceService.isMakerCheckerEnabledForTask()) - they are excluded from this screen for a
* different reason: UPDATE_PERMISSION governs this very "Configure Maker Checker Tasks" screen
* (toggling which tasks require maker-checker), and UPDATE_CONFIGURATION governs the global
* maker-checker on/off switch and other System -> Configurations settings. If either were flipped
* to can_maker_checker=true and the approving checker.user lacked CHECKER_SUPER_USER, an admin
* could lock themselves out of ever approving further changes to maker-checker configuration
* itself, with no ordinary way to reverse it. Excluding them here only hides them from this
* screen; it does not touch their can_maker_checker value or their normal enforcement.
*/
private static final String SELF_REFERENTIAL_MAKER_CHECKER_CODES = "'UPDATE_PERMISSION','UPDATE_CONFIGURATION'";
public String makerCheckerablePermissionSchema() {
/*
* get all 'Maker-Checkerable' permissions - Maintenance permissions (i.e. exclude the 'special' grouping,
* the READ permissions and the CHECKER permissions), and exclude permission codes whose approval
* workflow does not actually read can_maker_checker (see DEAD_MAKER_CHECKER_CODES above).
* workflow does not actually read can_maker_checker (see DEAD_MAKER_CHECKER_CODES above), as well as
* codes that control maker-checker configuration itself (see SELF_REFERENTIAL_MAKER_CHECKER_CODES
* above) to avoid a self-lockout.
*/
return "select p.grouping, p.code, p.entity_name as entityName, p.action_name as actionName, p.can_maker_checker as selected"
+ " from m_permission p " + " where " + sqlGenerator.escape("grouping")
+ " != 'special' and code not like 'READ_%' and code not like '%\\_CHECKER'"
+ " and code not in (" + DEAD_MAKER_CHECKER_CODES + ")"
+ " and code not in (" + SELF_REFERENTIAL_MAKER_CHECKER_CODES + ")"
+ " order by p.grouping, coalesce(entity_name, ''), p.code";
}
@@ -149,4 +149,7 @@
dev-latest's highest, 0582) to avoid ambiguity; content/semantics unchanged, only the file
name and this changeset's id were updated to match. -->
<include file="parts/0583_add_loan_originator_checker_permissions.xml" relativeToChangelogFile="true"/>
<include file="parts/0584_add_manage_survey_permissions.xml" relativeToChangelogFile="true"/>
<include file="parts/0585_add_loan_cob_catchup_permission.xml" relativeToChangelogFile="true"/>
<include file="parts/0586_add_system_module_checker_permissions.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
@@ -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">
<!-- The Manage Surveys screen (SPM scorecards, spm.api.SpmApiResource) previously performed
create/update/activate/deactivate directly against SpmService with only a login check -
no permission check at any layer. These 4 write operations have now been wired into the
standard command pipeline (CommandWrapperBuilder.createSurvey/updateSurvey/activateSurvey/
deactivateSurvey -> CreateSurveyCommandHandler/UpdateSurveyCommandHandler/
ActivateSurveyCommandHandler/DeactivateSurveyCommandHandler, entity "SURVEY"), which requires
these permission rows to exist so PortfolioCommandSourceWritePlatformServiceImpl.logCommandSource()
can authorize the caller. Seeded with can_maker_checker=false: no prior maker-checker behaviour
existed for this screen, and enabling maker-checker is a business decision left for later. -->
<changeSet id="0584_add_manage_survey_permissions" author="cbs-anshul">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'CREATE_SURVEY'</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="configuration"/>
<column name="code" value="CREATE_SURVEY"/>
<column name="entity_name" value="SURVEY"/>
<column name="action_name" value="CREATE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="configuration"/>
<column name="code" value="UPDATE_SURVEY"/>
<column name="entity_name" value="SURVEY"/>
<column name="action_name" value="UPDATE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="configuration"/>
<column name="code" value="ACTIVATE_SURVEY"/>
<column name="entity_name" value="SURVEY"/>
<column name="action_name" value="ACTIVATE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="configuration"/>
<column name="code" value="DEACTIVATE_SURVEY"/>
<column name="entity_name" value="SURVEY"/>
<column name="action_name" value="DEACTIVATE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
<!-- No role previously held any permission for this screen (it had no permission check at all),
so there are no "roles that already hold the base permission" to copy from. Following the
0511_institution_and_branch_profile.xml convention for a brand-new permission model: grant to
Super user plus any manager-named role. -->
<changeSet id="0584_grant_manage_survey_permissions" author="cbs-anshul">
<sql>
INSERT INTO m_role_permission (role_id, permission_id)
SELECT r.id, p.id
FROM m_role r
JOIN m_permission p ON p.code IN ('CREATE_SURVEY', 'UPDATE_SURVEY', 'ACTIVATE_SURVEY', 'DEACTIVATE_SURVEY')
WHERE (LOWER(r.name) LIKE '%manager%' OR LOWER(r.name) = 'super user')
AND NOT EXISTS (
SELECT 1 FROM m_role_permission rp2
WHERE rp2.role_id = r.id AND rp2.permission_id = p.id
)
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,44 @@
<?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">
<!-- LoanCOBCatchUpApiResource.executeLoanCOBCatchUp() (POST /v1/loans/catch-up) previously had no
permission check at all. A validateHasPermissionTo("EXECUTE_LOANCOBCATCHUP") check was added
directly (not full CommandWrapper/maker-checker wiring, since this is a job-execution trigger,
not a CRUD operation on a persisted entity - matching the existing EXECUTE_INLINE_JOB precedent),
which requires this permission row to exist. can_maker_checker=false: this closes the
authorization gap only, it does not introduce maker-checker for job execution. -->
<changeSet id="0585_add_loan_cob_catchup_permission" author="cbs-anshul">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'EXECUTE_LOANCOBCATCHUP'</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="organisation"/>
<column name="code" value="EXECUTE_LOANCOBCATCHUP"/>
<column name="entity_name" value="LOANCOBCATCHUP"/>
<column name="action_name" value="EXECUTE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
<!-- No role previously held any permission for this endpoint (it had no permission check at all),
so there are no "roles that already hold the base permission" to copy from. Following the
0511_institution_and_branch_profile.xml convention for a brand-new permission model: grant to
Super user plus any manager-named role. -->
<changeSet id="0585_grant_loan_cob_catchup_permission" author="cbs-anshul">
<sql>
INSERT INTO m_role_permission (role_id, permission_id)
SELECT r.id, p.id
FROM m_role r
JOIN m_permission p ON p.code = 'EXECUTE_LOANCOBCATCHUP'
WHERE (LOWER(r.name) LIKE '%manager%' OR LOWER(r.name) = 'super user')
AND NOT EXISTS (
SELECT 1 FROM m_role_permission rp2
WHERE rp2.role_id = r.id AND rp2.permission_id = p.id
)
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,107 @@
<?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">
<!-- The Admin -> System maker-checker audit found 8 base permissions whose "_CHECKER" counterpart
row was never seeded (AppUser.validateHasCheckerPermissionTo() computes
checkerPermissionName = code.toUpperCase() + "_CHECKER" and requires the approver to hold
either CHECKER_SUPER_USER or that exact permission code - see the identical issue already
fixed for LOAN_ORIGINATOR in 0583_add_loan_originator_checker_permissions.xml). Without these
rows, no ordinary role can ever be granted checker rights for these operations, and
UPDATE_BATCH_BUSINESS_STEP (Manage Jobs - COB Workflow Step Reorder) is the one permission in
all of Admin -> System that already has can_maker_checker=true today, making its missing
checker row a live gap, not merely a structural one. All 8 rows are seeded with
can_maker_checker=false: this changeset only makes the approval path structurally ready, it
does not itself enable maker-checker on any base permission.
For CREATE/UPDATE/DELETE_ACCOUNTNUMBERFORMAT_CHECKER, entity_name is set to
"ACCOUNTNUMBERFORMAT" for all three (matching CREATE_ACCOUNTNUMBERFORMAT and
UPDATE_ACCOUNTNUMBERFORMAT). The pre-existing base permission DELETE_ACCOUNTNUMBERFORMAT has
entity_name="HOOK" (a pre-existing data bug, confirmed via read-only query, left untouched -
fixing an existing permission row's value was out of scope for this change). This checker row
deliberately uses the correct entity name rather than propagating that bug. -->
<changeSet id="0586_add_system_module_checker_permissions" author="cbs-anshul">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'CREATE_ACCOUNTNUMBERFORMAT_CHECKER'</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="configuration"/>
<column name="code" value="CREATE_ACCOUNTNUMBERFORMAT_CHECKER"/>
<column name="entity_name" value="ACCOUNTNUMBERFORMAT"/>
<column name="action_name" value="CREATE_CHECKER"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="configuration"/>
<column name="code" value="UPDATE_ACCOUNTNUMBERFORMAT_CHECKER"/>
<column name="entity_name" value="ACCOUNTNUMBERFORMAT"/>
<column name="action_name" value="UPDATE_CHECKER"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="configuration"/>
<column name="code" value="DELETE_ACCOUNTNUMBERFORMAT_CHECKER"/>
<column name="entity_name" value="ACCOUNTNUMBERFORMAT"/>
<column name="action_name" value="DELETE_CHECKER"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="infrastructure"/>
<column name="code" value="CREATE_ENTITYMAPPING_CHECKER"/>
<column name="entity_name" value="ENTITYMAPPING"/>
<column name="action_name" value="CREATE_CHECKER"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="infrastructure"/>
<column name="code" value="UPDATE_ENTITYMAPPING_CHECKER"/>
<column name="entity_name" value="ENTITYMAPPING"/>
<column name="action_name" value="UPDATE_CHECKER"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="infrastructure"/>
<column name="code" value="DELETE_ENTITYMAPPING_CHECKER"/>
<column name="entity_name" value="ENTITYMAPPING"/>
<column name="action_name" value="DELETE_CHECKER"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="organisation"/>
<column name="code" value="CREATE_USERAGREEMENT_CHECKER"/>
<column name="entity_name" value="USERAGREEMENT"/>
<column name="action_name" value="CREATE_CHECKER"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="organisation"/>
<column name="code" value="UPDATE_BATCH_BUSINESS_STEP_CHECKER"/>
<column name="entity_name" value="BATCH_BUSINESS_STEP"/>
<column name="action_name" value="UPDATE_CHECKER"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
<!-- Granted to whichever roles already hold the corresponding base permission - the same
"grant to roles that already hold the base permission" convention used in
0583_add_loan_originator_checker_permissions.xml. -->
<changeSet id="0586_grant_system_module_checker_permissions" author="cbs-anshul">
<sql>
INSERT INTO m_role_permission (role_id, permission_id)
SELECT rp.role_id, checker.id
FROM m_role_permission rp
JOIN m_permission base ON base.id = rp.permission_id
JOIN m_permission checker ON checker.code = base.code || '_CHECKER'
WHERE base.code IN ('CREATE_ACCOUNTNUMBERFORMAT', 'UPDATE_ACCOUNTNUMBERFORMAT', 'DELETE_ACCOUNTNUMBERFORMAT',
'CREATE_ENTITYMAPPING', 'UPDATE_ENTITYMAPPING', 'DELETE_ENTITYMAPPING',
'CREATE_USERAGREEMENT', 'UPDATE_BATCH_BUSINESS_STEP')
AND NOT EXISTS (
SELECT 1 FROM m_role_permission rp2
WHERE rp2.role_id = rp.role_id AND rp2.permission_id = checker.id
)
</sql>
</changeSet>
</databaseChangeLog>