This is an automated email from the ASF dual-hosted git repository.

adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git


The following commit(s) were added to refs/heads/develop by this push:
     new 53f2cd4358 FINERACT-274: Fixed orphaned datatable after maker checker 
rejection
53f2cd4358 is described below

commit 53f2cd4358d49a8d1317bcf6773f25ec6dece4e0
Author: edk12564 <[email protected]>
AuthorDate: Sat Feb 14 20:26:27 2026 -0600

    FINERACT-274: Fixed orphaned datatable after maker checker rejection
    
    - added datatable cleanup service after a maker checker rejection
    - checks if datatable was created in rejected command source, and if so 
deletes it
    - added integration test to assess orphan table deletion
    - accounted for MariaDB and PostgreSQL
    - accounted for group, center, loan account, office, and savings account 
levels
    - utilized more generic interface with options for more extensibility
---
 ...folioCommandSourceWritePlatformServiceImpl.java |  8 +++
 .../dataqueries/service/CleanupService.java        | 27 ++++++++
 .../service/DatatableRejectionCleanupService.java  | 55 ++++++++++++++++
 .../integrationtests/MakercheckerTest.java         | 76 ++++++++++++++++++++++
 .../common/commands/MakercheckersHelper.java       |  7 ++
 5 files changed, 173 insertions(+)

diff --git 
a/fineract-core/src/main/java/org/apache/fineract/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java
 
b/fineract-core/src/main/java/org/apache/fineract/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java
index 5406ac5b44..604a07a7a4 100644
--- 
a/fineract-core/src/main/java/org/apache/fineract/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java
+++ 
b/fineract-core/src/main/java/org/apache/fineract/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java
@@ -19,6 +19,7 @@
 package org.apache.fineract.commands.service;
 
 import com.google.gson.JsonElement;
+import java.util.List;
 import java.util.Objects;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -32,6 +33,7 @@ import 
org.apache.fineract.infrastructure.configuration.domain.ConfigurationDoma
 import org.apache.fineract.infrastructure.core.api.JsonCommand;
 import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
 import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
+import org.apache.fineract.infrastructure.dataqueries.service.CleanupService;
 import 
org.apache.fineract.infrastructure.jobs.service.SchedulerJobRunnerReadService;
 import 
org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
 import org.apache.fineract.useradministration.domain.AppUser;
@@ -49,6 +51,7 @@ public class PortfolioCommandSourceWritePlatformServiceImpl 
implements Portfolio
     private final CommandProcessingService processAndLogCommandService;
     private final SchedulerJobRunnerReadService schedulerJobRunnerReadService;
     private final ConfigurationDomainService configurationService;
+    private final List<CleanupService> cleanupServices;
 
     @Override
     public CommandProcessingResult logCommandSource(final CommandWrapper 
wrapper) {
@@ -146,6 +149,11 @@ public class 
PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio
         final AppUser maker = this.context.authenticatedUser();
         commandSourceInput.markAsRejected(maker);
         this.commandSourceRepository.save(commandSourceInput);
+        if (cleanupServices != null) {
+            for (CleanupService cleanupService : cleanupServices) {
+                cleanupService.cleanup(commandSourceInput);
+            }
+        }
         return makerCheckerId;
     }
 }
diff --git 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/CleanupService.java
 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/CleanupService.java
new file mode 100644
index 0000000000..175a3a18b2
--- /dev/null
+++ 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/CleanupService.java
@@ -0,0 +1,27 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.dataqueries.service;
+
+import org.apache.fineract.commands.domain.CommandSource;
+
+public interface CleanupService {
+
+    void cleanup(CommandSource commandSource);
+
+}
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/DatatableRejectionCleanupService.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/DatatableRejectionCleanupService.java
new file mode 100644
index 0000000000..c49fc6f266
--- /dev/null
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/dataqueries/service/DatatableRejectionCleanupService.java
@@ -0,0 +1,55 @@
+/**
+ * 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.infrastructure.dataqueries.service;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.commands.domain.CommandSource;
+import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
+import 
org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class DatatableRejectionCleanupService implements CleanupService {
+
+    private final JdbcTemplate jdbcTemplate;
+    private final DatabaseSpecificSQLGenerator sqlGenerator;
+    private final FromJsonHelper fromJsonHelper;
+
+    @Override
+    public void cleanup(CommandSource commandSource) {
+
+        boolean isCreateAction = 
"CREATE".equals(commandSource.getActionName());
+        boolean isDatatableEntity = 
"DATATABLE".equals(commandSource.getEntityName());
+        if (!isCreateAction || !isDatatableEntity) {
+            return;
+        }
+
+        final String datatableName = 
fromJsonHelper.parse(commandSource.getCommandAsJson()).getAsJsonObject().get("datatableName")
+                .getAsString();
+
+        final String sql = "DROP TABLE IF EXISTS " + 
sqlGenerator.escape(datatableName);
+        log.info("Cleaning up orphaned datatable after rejection: {}", 
datatableName);
+        jdbcTemplate.execute(sql);
+
+    }
+}
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/MakercheckerTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/MakercheckerTest.java
index 91dd1d097e..a6a92a6415 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/MakercheckerTest.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/MakercheckerTest.java
@@ -36,16 +36,20 @@ import 
org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationC
 import org.apache.fineract.integrationtests.common.AuditHelper;
 import org.apache.fineract.integrationtests.common.ClientHelper;
 import org.apache.fineract.integrationtests.common.CommonConstants;
+import org.apache.fineract.integrationtests.common.FineractClientHelper;
 import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper;
 import org.apache.fineract.integrationtests.common.Utils;
 import 
org.apache.fineract.integrationtests.common.commands.MakercheckersHelper;
 import org.apache.fineract.integrationtests.common.organisation.StaffHelper;
 import 
org.apache.fineract.integrationtests.common.savings.SavingsAccountHelper;
 import 
org.apache.fineract.integrationtests.common.savings.SavingsProductHelper;
+import org.apache.fineract.integrationtests.common.system.DatatableHelper;
 import 
org.apache.fineract.integrationtests.useradministration.roles.RolesHelper;
 import 
org.apache.fineract.integrationtests.useradministration.users.UserHelper;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 
 @SuppressWarnings({ "unused" })
 public class MakercheckerTest {
@@ -195,6 +199,78 @@ public class MakercheckerTest {
         }
     }
 
+    @ParameterizedTest
+    @ValueSource(strings = { "m_client", "m_group", "m_center", "m_loan", 
"m_office", "m_savings_account" })
+    public void testRejectDatatableCreationCleansUpOrphanedTable(String 
apptableName) {
+
+        // enable maker-checker globally
+        
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.MAKER_CHECKER,
+                new PutGlobalConfigurationsRequest().enabled(true));
+        
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_SAME_MAKER_CHECKER,
+                new PutGlobalConfigurationsRequest().enabled(false));
+
+        try {
+            // enable maker-checker for datatable creation
+            PutPermissionsRequest putPermissionsRequest = new 
PutPermissionsRequest().putPermissionsItem("CREATE_DATATABLE", true);
+            rolesHelper.updatePermissions(putPermissionsRequest);
+
+            // create role with permissions for maker and checker
+            Integer roleId = RolesHelper.createRole(requestSpec, responseSpec);
+            Map<String, Boolean> permissionMap = Map.of("CREATE_DATATABLE", 
true, "CREATE_DATATABLE_CHECKER", true);
+            RolesHelper.addPermissionsToRole(requestSpec, responseSpec, 
roleId, permissionMap);
+
+            // create maker user
+            Integer staffId = StaffHelper.createStaff(this.requestSpec, 
this.responseSpec);
+            String maker = Utils.uniqueRandomStringGenerator("user", 8);
+            Integer makerUserId = (Integer) 
UserHelper.createUser(this.requestSpec, this.responseSpec, roleId, staffId, 
maker,
+                    "A1b2c3d4e5f$", "resourceId");
+
+            // create checker user
+            String checker = Utils.uniqueRandomStringGenerator("user", 8);
+            UserHelper.createUser(this.requestSpec, this.responseSpec, roleId, 
staffId, checker, "A1b2c3d4e5f$", "resourceId");
+
+            RequestSpecification makerRequestSpec = new 
RequestSpecBuilder().setContentType(ContentType.JSON).build()
+                    .header("Authorization", "Basic " + 
Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey(maker, 
"A1b2c3d4e5f$"));
+
+            // maker creates datatable with maker-checker enabled, this 
creates the physical table but queues for
+            // approval
+            DatatableHelper makerDatatableHelper = new 
DatatableHelper(makerRequestSpec, this.responseSpec);
+            String datatableJson = 
DatatableHelper.getTestDatatableAsJSON(apptableName, false);
+            String datatableName = 
com.google.gson.JsonParser.parseString(datatableJson).getAsJsonObject().get("datatableName")
+                    .getAsString();
+            makerDatatableHelper.createDatatable(datatableJson, "");
+
+            // find the pending command
+            List<Map<String, Object>> auditDetails = makercheckersHelper
+                    .getMakerCheckerList(Map.of("actionName", "CREATE", 
"entityName", "DATATABLE", "makerId", makerUserId.toString()));
+            assertEquals(1, auditDetails.size(), "Error: Expected only one 
pending CREATE DATATABLE command");
+            Long commandId = ((Double) 
auditDetails.get(0).get("id")).longValue();
+
+            // checker rejects the command which should drop the orphaned table
+            
MakercheckersHelper.rejectMakerCheckerEntry(FineractClientHelper.createNewFineractClient(checker,
 "A1b2c3d4e5f$"), commandId);
+
+            // verify the datatable no longer exists by trying to create it 
again
+            // verify without maker checker, so transaction rollback in 
postgres doesn't break the test
+            putPermissionsRequest = new 
PutPermissionsRequest().putPermissionsItem("CREATE_DATATABLE", false);
+            rolesHelper.updatePermissions(putPermissionsRequest);
+
+            DatatableHelper adminDatatableHelper = new 
DatatableHelper(this.requestSpec, this.responseSpec);
+            String recreatedName = 
adminDatatableHelper.createDatatable(datatableJson, "resourceIdentifier");
+            assertEquals(datatableName, recreatedName, "Error: Was not able to 
recreate datatable after rejection cleanup");
+
+            // cleanup after test
+            adminDatatableHelper.deleteDatatable(datatableName);
+        } finally {
+            
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.MAKER_CHECKER,
+                    new PutGlobalConfigurationsRequest().enabled(false));
+            
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_SAME_MAKER_CHECKER,
+                    new PutGlobalConfigurationsRequest().enabled(true));
+
+            PutPermissionsRequest putPermissionsRequest = new 
PutPermissionsRequest().putPermissionsItem("CREATE_DATATABLE", false);
+            rolesHelper.updatePermissions(putPermissionsRequest);
+        }
+    }
+
     private Integer createSavingsProductDailyPosting() {
         final String savingsProductJSON = 
this.savingsProductHelper.withInterestCompoundingPeriodTypeAsDaily()
                 
.withInterestPostingPeriodTypeAsDaily().withInterestCalculationPeriodTypeAsDailyBalance().build();
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/commands/MakercheckersHelper.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/commands/MakercheckersHelper.java
index 9e3c740d7b..22cbaec5af 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/commands/MakercheckersHelper.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/commands/MakercheckersHelper.java
@@ -26,6 +26,9 @@ import java.lang.reflect.Type;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import org.apache.fineract.client.models.PostMakerCheckersResponse;
+import org.apache.fineract.client.util.Calls;
+import org.apache.fineract.client.util.FineractClient;
 import org.apache.fineract.client.util.JSON;
 import org.apache.fineract.integrationtests.common.Utils;
 
@@ -80,4 +83,8 @@ public class MakercheckersHelper {
         String url = MAKERCHECKER_URL + "/" + auditId + "?command=approve&" + 
Utils.TENANT_IDENTIFIER;
         return Utils.performServerPost(requestSpec, responseSpec, url, "", "");
     }
+
+    public static PostMakerCheckersResponse 
rejectMakerCheckerEntry(FineractClient client, Long auditId) {
+        return Calls.ok(client.makerCheckers.approveMakerCheckerEntry(auditId, 
"reject"));
+    }
 }

Reply via email to