From d07422d25ae08f7fcf2619a9a5aa46ab13db66e7 Mon Sep 17 00:00:00 2001 From: Sneha Date: Wed, 1 Jul 2026 17:04:01 +0530 Subject: [PATCH 01/10] fix: amm-2368 item to shop mapping issue --- .../itemfacilitymapping/MItemFacilityMappingController.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/iemr/admin/controller/itemfacilitymapping/MItemFacilityMappingController.java b/src/main/java/com/iemr/admin/controller/itemfacilitymapping/MItemFacilityMappingController.java index 0bf5bd5..c0f662c 100644 --- a/src/main/java/com/iemr/admin/controller/itemfacilitymapping/MItemFacilityMappingController.java +++ b/src/main/java/com/iemr/admin/controller/itemfacilitymapping/MItemFacilityMappingController.java @@ -93,6 +93,7 @@ public String mapItemtoStrore(@RequestBody String mapItemtoStrore) { resList.add(resDataMap); } + itemDetailsIndex++; } ArrayList data = M_itemfacilitymappingInter.mapItemtoStore(resList); From 931c56b2b7fefc6f4d0df70cb7daa3ab4605832f Mon Sep 17 00:00:00 2001 From: Vanitha S <116701245+vanitha1822@users.noreply.github.com> Date: Tue, 14 Jul 2026 11:57:43 +0530 Subject: [PATCH 02/10] Fix the Health Response Format (#138) * fix: health / version response format * chore: remove unused VersionService after /version rewrite VersionController now reads git.properties directly, so the old build-info- based VersionService is dead code. Co-Authored-By: Claude Opus 4.8 (1M context) * fix: force JSON response for /health endpoint Without an explicit produces type, Spring content-negotiated /health to XML when the browser's Accept header preferred it (Jackson XML is on the classpath), rendering the raw XML tree in-browser. Pin the endpoint to application/json. Co-Authored-By: Claude Opus 4.8 (1M context) * fix: pom version --------- Co-authored-by: Claude Opus 4.8 (1M context) --- Dockerfile | 2 +- pom.xml | 2 +- .../controller/health/HealthController.java | 7 +- .../controller/version/VersionController.java | 25 +- .../admin/service/health/HealthService.java | 778 ++++++++---------- .../admin/service/version/VersionService.java | 170 ---- 6 files changed, 354 insertions(+), 630 deletions(-) delete mode 100644 src/main/java/com/iemr/admin/service/version/VersionService.java diff --git a/Dockerfile b/Dockerfile index f99697f..9073787 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ COPY . . # Build the application while caching Maven dependencies to speed up future builds RUN --mount=type=cache,target=/root/.m2 \ - mvn clean package -DENV_VAR=docker -DskipTests -Dgit.skip=true + mvn clean package -DENV_VAR=docker -DskipTests # --- Stage 2: Run the application with a minimal JRE image --- FROM eclipse-temurin:17-jre diff --git a/pom.xml b/pom.xml index 4f180be..062df58 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.iemr.admin admin-api - 3.8.1 + 3.8.2 war Admin-API Admin Page diff --git a/src/main/java/com/iemr/admin/controller/health/HealthController.java b/src/main/java/com/iemr/admin/controller/health/HealthController.java index d1a9e80..3fa49ca 100644 --- a/src/main/java/com/iemr/admin/controller/health/HealthController.java +++ b/src/main/java/com/iemr/admin/controller/health/HealthController.java @@ -27,6 +27,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -52,8 +53,8 @@ public HealthController(HealthService healthService) { this.healthService = healthService; } - @GetMapping - @Operation(summary = "Check infrastructure health", + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(summary = "Check infrastructure health", description = "Returns the health status of MySQL, Redis, and other configured services") @ApiResponses({ @ApiResponse(responseCode = "200", description = "Services are UP or DEGRADED (operational with warnings)"), @@ -61,7 +62,7 @@ public HealthController(HealthService healthService) { }) public ResponseEntity> checkHealth() { logger.debug("Health check endpoint called"); - + try { Map healthStatus = healthService.checkHealth(); String overallStatus = (String) healthStatus.get("status"); diff --git a/src/main/java/com/iemr/admin/controller/version/VersionController.java b/src/main/java/com/iemr/admin/controller/version/VersionController.java index 15d14bb..387e861 100644 --- a/src/main/java/com/iemr/admin/controller/version/VersionController.java +++ b/src/main/java/com/iemr/admin/controller/version/VersionController.java @@ -1,8 +1,8 @@ /* -* AMRIT – Accessible Medical Records via Integrated Technology -* Integrated EHR (Electronic Health Records) Solution +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution * -* Copyright (C) "Piramal Swasthya Management and Research Institute" +* Copyright (C) "Piramal Swasthya Management and Research Institute" * * This file is part of AMRIT. * @@ -22,26 +22,29 @@ package com.iemr.admin.controller.version; +import java.io.InputStream; +import java.io.IOException; +import java.util.LinkedHashMap; import java.util.Map; +import java.util.Properties; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; + import io.swagger.v3.oas.annotations.Operation; -import java.io.IOException; -import java.io.InputStream; -import java.util.LinkedHashMap; -import java.util.Properties; -import org.springframework.http.MediaType; @RestController public class VersionController { - private final Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName()); - + private final Logger logger = + LoggerFactory.getLogger(this.getClass().getSimpleName()); + private static final String UNKNOWN_VALUE = "unknown"; - + @Operation(summary = "Get version information") @GetMapping(value = "/version", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity> versionInformation() { diff --git a/src/main/java/com/iemr/admin/service/health/HealthService.java b/src/main/java/com/iemr/admin/service/health/HealthService.java index e6a0432..b679cd1 100644 --- a/src/main/java/com/iemr/admin/service/health/HealthService.java +++ b/src/main/java/com/iemr/admin/service/health/HealthService.java @@ -1,533 +1,423 @@ /* -* AMRIT – Accessible Medical Records via Integrated Technology -* Integrated EHR (Electronic Health Records) Solution -* -* Copyright (C) "Piramal Swasthya Management and Research Institute" -* -* This file is part of AMRIT. -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ - + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ package com.iemr.admin.service.health; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.stereotype.Service; + +import jakarta.annotation.PreDestroy; +import javax.sql.DataSource; import java.sql.Connection; -import java.sql.PreparedStatement; import java.sql.ResultSet; +import java.sql.Statement; import java.time.Instant; import java.util.LinkedHashMap; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.ExecutionException; -import java.util.function.Supplier; -import jakarta.annotation.PreDestroy; -import javax.sql.DataSource; -import com.zaxxer.hikari.HikariDataSource; -import com.zaxxer.hikari.HikariPoolMXBean; -import java.lang.management.ManagementFactory; -import javax.management.MBeanServer; -import javax.management.ObjectName; -import java.util.concurrent.locks.ReentrantReadWriteLock; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.stereotype.Service; - -import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; @Service public class HealthService { private static final Logger logger = LoggerFactory.getLogger(HealthService.class); - private static final String STATUS_KEY = "status"; - private static final String STATUS_UP = "UP"; - private static final String STATUS_DOWN = "DOWN"; - private static final String STATUS_DEGRADED = "DEGRADED"; - private static final String SEVERITY_KEY = "severity"; - private static final String SEVERITY_OK = "OK"; - private static final String SEVERITY_WARNING = "WARNING"; + // Event log constants + private static final String LOG_EVENT_STUCK_PROCESS = "MYSQL_STUCK_PROCESS"; + private static final String LOG_EVENT_LOCK_WAIT = "MYSQL_LOCK_WAIT"; + private static final String LOG_EVENT_DEADLOCK = "MYSQL_DEADLOCK"; + private static final String LOG_EVENT_SLOW_QUERIES = "MYSQL_SLOW_QUERIES"; + private static final String LOG_EVENT_CONN_USAGE = "MYSQL_CONNECTION_USAGE"; + private static final String LOG_EVENT_POOL_EXHAUSTED = "MYSQL_POOL_EXHAUSTED"; + + // Response field constants + private static final String FIELD_STATUS = "status"; + private static final String FIELD_SEVERITY = "severity"; + private static final String FIELD_MYSQL = "mysql"; + private static final String FIELD_REDIS = "redis"; + private static final String FIELD_CHECKED_AT = "checkedAt"; + + // Severity constants private static final String SEVERITY_CRITICAL = "CRITICAL"; - private static final String ERROR_KEY = "error"; - private static final String MESSAGE_KEY = "message"; - private static final String RESPONSE_TIME_KEY = "responseTimeMs"; - private static final long MYSQL_TIMEOUT_SECONDS = 3; - private static final long REDIS_TIMEOUT_SECONDS = 3; - - private static final long ADVANCED_CHECKS_THROTTLE_SECONDS = 30; - private static final long RESPONSE_TIME_THRESHOLD_MS = 2000; - - private static final String DIAGNOSTIC_LOCK_WAIT = "MYSQL_LOCK_WAIT"; - private static final String DIAGNOSTIC_SLOW_QUERIES = "MYSQL_SLOW_QUERIES"; - private static final String DIAGNOSTIC_POOL_EXHAUSTED = "MYSQL_POOL_EXHAUSTED"; - private static final String DIAGNOSTIC_LOG_TEMPLATE = "Diagnostic: {}"; - + private static final String SEVERITY_WARNING = "WARNING"; + private static final String SEVERITY_OK = "OK"; + private static final String SEVERITY_INFO = "INFO"; + + // Database query constants + private static final String STATUS_VALUE = "Value"; + private static final String STATUS_UP = "UP"; + private static final String STATUS_DOWN = "DOWN"; + private static final String STATUS_DEGRADED = "DEGRADED"; + private static final String STATUS_NOT_CONFIGURED = "NOT_CONFIGURED"; + + // Thresholds + private static final long RESPONSE_TIME_SLOW_MS = 2000; // > 2s → SLOW + private static final int STUCK_PROCESS_THRESHOLD = 5; // > 5 stuck → WARNING + private static final int STUCK_PROCESS_SECONDS = 30; // process age in seconds + private static final int LONG_TXN_WARNING_THRESHOLD = 1; // ≥1 long txn → WARNING + private static final int LONG_TXN_CRITICAL_THRESHOLD = 5; // ≥5 long txns → CRITICAL + private static final int LONG_TXN_SECONDS = 60; // transaction age threshold + private static final int CONNECTION_USAGE_WARNING = 80; // > 80% → WARNING + private static final int CONNECTION_USAGE_CRITICAL= 95; // > 95% → CRITICAL + private static final long DIAGNOSTIC_INTERVAL_SEC = 30; // background run interval + private static final long DIAGNOSTIC_GUARD_SEC = 25; // safety dedup guard private final DataSource dataSource; - private final RedisTemplate redisTemplate; - private final ExecutorService executorService; - - private volatile long lastAdvancedCheckTime = 0; - private volatile AdvancedCheckResult cachedAdvancedCheckResult = null; - private final ReentrantReadWriteLock advancedCheckLock = new ReentrantReadWriteLock(); - private final AtomicBoolean advancedCheckInProgress = new AtomicBoolean(false); - - @Value("${health.advanced.checks.enabled:true}") - private boolean advancedHealthChecksEnabled; - - public HealthService(DataSource dataSource, - @Autowired(required = false) RedisTemplate redisTemplate) { - this.dataSource = dataSource; - this.redisTemplate = redisTemplate; - this.executorService = Executors.newFixedThreadPool(2, new java.util.concurrent.ThreadFactory() { - private final AtomicInteger threadCounter = new AtomicInteger(0); - - @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(r, "health-check-" + threadCounter.incrementAndGet()); - t.setDaemon(true); - return t; - } + private final RedisConnectionFactory redisConnectionFactory; + + private final ScheduledExecutorService diagnosticScheduler = + Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "mysql-diagnostic-thread"); + t.setDaemon(true); + return t; }); + + private final AtomicLong lastDiagnosticRunAt = new AtomicLong(0); + private final AtomicReference cachedDbSeverity = + new AtomicReference<>(SEVERITY_OK); + private final AtomicLong previousDeadlockCount = new AtomicLong(0); + private final AtomicLong previousSlowQueryCount = new AtomicLong(0); + public HealthService(ObjectProvider dataSourceProvider, + ObjectProvider redisProvider) { + this.dataSource = dataSourceProvider.getIfAvailable(); + this.redisConnectionFactory = redisProvider.getIfAvailable(); + + // Start background diagnostics only if DB is configured. + // Initial delay = 0 so the first run happens at startup. + if (this.dataSource != null) { + diagnosticScheduler.scheduleAtFixedRate( + this::runAdvancedMySQLDiagnostics, + 0, + DIAGNOSTIC_INTERVAL_SEC, + TimeUnit.SECONDS + ); + } } @PreDestroy - public void shutdown() { - if (executorService != null && !executorService.isShutdown()) { - try { - executorService.shutdown(); - if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { - executorService.shutdownNow(); - logger.warn("ExecutorService did not terminate gracefully"); - } - } catch (InterruptedException e) { - executorService.shutdownNow(); - Thread.currentThread().interrupt(); - logger.warn("ExecutorService shutdown interrupted", e); + public void shutdownDiagnostics() { + logger.info("[HEALTH_SERVICE_SHUTDOWN] Shutting down diagnostic scheduler..."); + diagnosticScheduler.shutdown(); + try { + if (!diagnosticScheduler.awaitTermination(5, TimeUnit.SECONDS)) { + logger.warn("[HEALTH_SERVICE_SHUTDOWN] Diagnostic scheduler did not terminate gracefully"); + diagnosticScheduler.shutdownNow(); } + logger.info("[HEALTH_SERVICE_SHUTDOWN] Diagnostic scheduler shut down successfully"); + } catch (InterruptedException e) { + logger.error("[HEALTH_SERVICE_SHUTDOWN] Interrupted while shutting down scheduler", e); + diagnosticScheduler.shutdownNow(); + Thread.currentThread().interrupt(); } } - + + // PUBLIC — Called by the /health controller public Map checkHealth() { Map response = new LinkedHashMap<>(); - response.put("timestamp", Instant.now().toString()); - - Map mysqlStatus = new ConcurrentHashMap<>(); - Map redisStatus = new ConcurrentHashMap<>(); - - if (!executorService.isShutdown()) { - performHealthChecks(mysqlStatus, redisStatus); - } - - ensurePopulated(mysqlStatus, "MySQL"); - ensurePopulated(redisStatus, "Redis"); + + Map mysqlResult = checkDatabaseConnectivity(); + Map redisResult = checkRedisConnectivity(); + + String mysqlStatus = (String) mysqlResult.get(FIELD_STATUS); + String redisStatus = (String) redisResult.get(FIELD_STATUS); + + boolean overallUp = !STATUS_DOWN.equals(mysqlStatus) && !STATUS_DOWN.equals(redisStatus); + + response.put(FIELD_STATUS, overallUp ? STATUS_UP : STATUS_DOWN); + response.put(FIELD_CHECKED_AT, Instant.now().toString()); - Map> components = new LinkedHashMap<>(); - components.put("mysql", mysqlStatus); - components.put("redis", redisStatus); + // Expose only status and severity, keep diagnostics internal + Map mysqlSummary = new LinkedHashMap<>(); + mysqlSummary.put(FIELD_STATUS, mysqlResult.get(FIELD_STATUS)); + mysqlSummary.put(FIELD_SEVERITY, mysqlResult.get(FIELD_SEVERITY)); - response.put("components", components); - response.put(STATUS_KEY, computeOverallStatus(components)); + Map redisSummary = new LinkedHashMap<>(); + redisSummary.put(FIELD_STATUS, redisResult.get(FIELD_STATUS)); + redisSummary.put(FIELD_SEVERITY, redisResult.get(FIELD_SEVERITY)); + response.put(FIELD_MYSQL, mysqlSummary); + response.put(FIELD_REDIS, redisSummary); + return response; } - - private void performHealthChecks(Map mysqlStatus, Map redisStatus) { - Future mysqlFuture = null; - Future redisFuture = null; - try { - mysqlFuture = executorService.submit( - () -> performHealthCheck("MySQL", mysqlStatus, this::checkMySQLHealthSync)); - redisFuture = executorService.submit( - () -> performHealthCheck("Redis", redisStatus, this::checkRedisHealthSync)); - - awaitHealthChecks(mysqlFuture, redisFuture); - } catch (TimeoutException e) { - logger.warn("Health check aggregate timeout after {} seconds", getMaxTimeout()); - cancelFutures(mysqlFuture, redisFuture); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - logger.warn("Health check was interrupted"); - cancelFutures(mysqlFuture, redisFuture); - } catch (Exception e) { - logger.warn("Health check execution error: {}", e.getMessage()); - cancelFutures(mysqlFuture, redisFuture); + // Runs only SELECT 1 with a hard 3-second timeout on query execution. + // NOTE: getConnection() is NOT bounded by this timeout — it respects the pool's + // connectionTimeout (default 30s in HikariCP). For true 3-second /health guarantees, + // configure the DataSource connectionTimeout ≤ 3 seconds or wrap in an ExecutorService timeout. + private Map checkDatabaseConnectivity() { + Map result = new LinkedHashMap<>(); + + if (dataSource == null) { + result.put(FIELD_STATUS, STATUS_NOT_CONFIGURED); + result.put(FIELD_SEVERITY, SEVERITY_INFO); + return result; } - } - private void awaitHealthChecks(Future mysqlFuture, Future redisFuture) throws TimeoutException, InterruptedException, ExecutionException { - long maxTimeout = getMaxTimeout(); - long deadlineNs = System.nanoTime() + TimeUnit.SECONDS.toNanos(maxTimeout); - - mysqlFuture.get(maxTimeout, TimeUnit.SECONDS); - long remainingNs = deadlineNs - System.nanoTime(); - - if (remainingNs > 0) { - redisFuture.get(remainingNs, TimeUnit.NANOSECONDS); - } else { - redisFuture.cancel(true); + try (Connection conn = dataSource.getConnection(); + Statement stmt = conn.createStatement()) { + + stmt.setQueryTimeout(3); // Bounds only the SELECT 1 execution + stmt.execute("SELECT 1"); + + // If SELECT 1 succeeds, use cached severity from background diagnostics + String severity = cachedDbSeverity.get(); + result.put(FIELD_STATUS, resolveDatabaseStatus(severity)); + result.put(FIELD_SEVERITY, severity); + + } catch (Exception e) { + // Log connection failure as a structured event + logger.error( + "[MYSQL_CONNECT_FAILED] MySQL connectivity check failed | error=\"{}\"", + e.getMessage() + ); + + result.put(FIELD_STATUS, STATUS_DOWN); + result.put(FIELD_SEVERITY, SEVERITY_CRITICAL); } - } - private long getMaxTimeout() { - return Math.max(MYSQL_TIMEOUT_SECONDS, REDIS_TIMEOUT_SECONDS) + 1; + return result; } - private void cancelFutures(Future mysqlFuture, Future redisFuture) { - if (mysqlFuture != null) mysqlFuture.cancel(true); - if (redisFuture != null) redisFuture.cancel(true); - } + private Map checkRedisConnectivity() { + Map result = new LinkedHashMap<>(); - private void ensurePopulated(Map status, String componentName) { + if (redisConnectionFactory == null) { + result.put(FIELD_STATUS, STATUS_NOT_CONFIGURED); + result.put(FIELD_SEVERITY, SEVERITY_INFO); + return result; + } - status.putIfAbsent(STATUS_KEY, STATUS_DOWN); - status.putIfAbsent(SEVERITY_KEY, SEVERITY_CRITICAL); - status.putIfAbsent(ERROR_KEY, componentName + " health check did not complete in time"); - } + try (RedisConnection conn = redisConnectionFactory.getConnection()) { + conn.ping(); + result.put(FIELD_STATUS, STATUS_UP); + result.put(FIELD_SEVERITY, SEVERITY_OK); - private HealthCheckResult checkMySQLHealthSync() { - try (Connection connection = dataSource.getConnection(); - PreparedStatement stmt = connection.prepareStatement("SELECT 1 as health_check")) { - - stmt.setQueryTimeout((int) MYSQL_TIMEOUT_SECONDS); - - try (ResultSet rs = stmt.executeQuery()) { - if (rs.next()) { - // Connection is auto-closed by try-with-resources here - // Advanced checks will open a separate connection if needed - } - } - } catch (Exception e) { - logger.warn("MySQL health check failed: {}", e.getMessage(), e); - return new HealthCheckResult(false, "MySQL connection failed", false); + logger.error( + "[REDIS_CONNECT_FAILED] Redis connectivity check failed | error=\"{}\"", + e.getMessage() + ); + + result.put(FIELD_STATUS, STATUS_DOWN); + result.put(FIELD_SEVERITY, SEVERITY_CRITICAL); } - - boolean isDegraded = performAdvancedMySQLChecksWithThrottle(); - return new HealthCheckResult(true, null, isDegraded); + return result; } - private HealthCheckResult checkRedisHealthSync() { - if (redisTemplate == null) { - return new HealthCheckResult(true, "Redis not configured — skipped", false); + private void runAdvancedMySQLDiagnostics() { + // Dedup guard: skip if last run was within the past 25 seconds + long now = System.currentTimeMillis(); + if (now - lastDiagnosticRunAt.get() < TimeUnit.SECONDS.toMillis(DIAGNOSTIC_GUARD_SEC)) { + return; } - - try { - String pong = redisTemplate.execute((org.springframework.data.redis.core.RedisCallback) (connection) -> connection.ping()); - - if ("PONG".equals(pong)) { - return new HealthCheckResult(true, null, false); - } - - return new HealthCheckResult(false, "Redis PING failed", false); - + lastDiagnosticRunAt.set(now); + + String worstSeverity = SEVERITY_OK; + + try (Connection conn = dataSource.getConnection()) { + worstSeverity = escalate(worstSeverity, performStuckProcessCheck(conn)); + worstSeverity = escalate(worstSeverity, performLongTransactionCheck(conn)); + worstSeverity = escalate(worstSeverity, performDeadlockCheck(conn)); + worstSeverity = escalate(worstSeverity, performSlowQueryCheck(conn)); + worstSeverity = escalate(worstSeverity, performConnectionUsageCheck(conn)); + } catch (Exception e) { - logger.warn("Redis health check failed: {}", e.getMessage(), e); - return new HealthCheckResult(false, "Redis connection failed", false); + logger.error( + "[MYSQL_DIAGNOSTIC_ERROR] Could not open connection for diagnostics | error=\"{}\"", + e.getMessage() + ); + worstSeverity = SEVERITY_CRITICAL; } + + cachedDbSeverity.set(worstSeverity); + logger.debug( + "[MYSQL_DIAGNOSTIC_COMPLETE] Background diagnostic cycle complete | severity={}", + worstSeverity + ); } - private Map performHealthCheck(String componentName, - Map status, - Supplier checker) { - long startTime = System.currentTimeMillis(); - - try { - HealthCheckResult result = checker.get(); - long responseTime = System.currentTimeMillis() - startTime; - - String componentStatus; - if (!result.isHealthy) { - componentStatus = STATUS_DOWN; - } else if (result.isDegraded) { - componentStatus = STATUS_DEGRADED; - } else { - componentStatus = STATUS_UP; - } - status.put(STATUS_KEY, componentStatus); - - status.put(RESPONSE_TIME_KEY, responseTime); - - String severity = determineSeverity(result.isHealthy, responseTime, result.isDegraded); - status.put(SEVERITY_KEY, severity); + private String performStuckProcessCheck(Connection conn) { + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT COUNT(*) AS cnt FROM information_schema.PROCESSLIST " + + "WHERE TIME > " + STUCK_PROCESS_SECONDS + " AND COMMAND != 'Sleep'")) { - if (result.error != null) { - String fieldKey = result.isHealthy ? MESSAGE_KEY : ERROR_KEY; - status.put(fieldKey, result.error); + if (rs.next()) { + int stuckCount = rs.getInt("cnt"); + if (stuckCount > 0) { + if (stuckCount > STUCK_PROCESS_THRESHOLD) { + logger.warn( + "[{}] Stuck MySQL processes detected above threshold | count={} | threshold={} | thresholdSeconds={}", + LOG_EVENT_STUCK_PROCESS, stuckCount, STUCK_PROCESS_THRESHOLD, STUCK_PROCESS_SECONDS + ); + return SEVERITY_WARNING; + } else { + logger.info( + "[{}] Stuck MySQL processes below threshold | count={} | threshold={} | thresholdSeconds={}", + LOG_EVENT_STUCK_PROCESS, stuckCount, STUCK_PROCESS_THRESHOLD, STUCK_PROCESS_SECONDS + ); + } + } } - - return status; - } catch (Exception e) { - long responseTime = System.currentTimeMillis() - startTime; - logger.error("{} health check failed with exception: {}", componentName, e.getMessage(), e); - - status.put(STATUS_KEY, STATUS_DOWN); - status.put(RESPONSE_TIME_KEY, responseTime); - status.put(SEVERITY_KEY, SEVERITY_CRITICAL); - status.put(ERROR_KEY, "Health check failed with an unexpected error"); - - return status; - } - } - - private String determineSeverity(boolean isHealthy, long responseTimeMs, boolean isDegraded) { - if (!isHealthy) { - return SEVERITY_CRITICAL; - } - - if (isDegraded) { - return SEVERITY_WARNING; + logger.error("[MYSQL_DIAGNOSTIC_ERROR] Stuck process check failed | error=\"{}\"", + e.getMessage()); } - - if (responseTimeMs > RESPONSE_TIME_THRESHOLD_MS) { - return SEVERITY_WARNING; - } - return SEVERITY_OK; } - private String computeOverallStatus(Map> components) { - boolean hasCritical = false; - boolean hasDegraded = false; - - for (Map componentStatus : components.values()) { - String status = (String) componentStatus.get(STATUS_KEY); - String severity = (String) componentStatus.get(SEVERITY_KEY); - - if (STATUS_DOWN.equals(status) || SEVERITY_CRITICAL.equals(severity)) { - hasCritical = true; - } - - if (STATUS_DEGRADED.equals(status)) { - hasDegraded = true; - } + private String performLongTransactionCheck(Connection conn) { + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT COUNT(*) AS cnt FROM information_schema.INNODB_TRX " + + "WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > " + LONG_TXN_SECONDS)) { - if (SEVERITY_WARNING.equals(severity)) { - hasDegraded = true; - } - } - - if (hasCritical) { - return STATUS_DOWN; - } - - if (hasDegraded) { - return STATUS_DEGRADED; - } - - return STATUS_UP; - } - - private boolean performAdvancedMySQLChecksWithThrottle() { - if (!advancedHealthChecksEnabled) { - return false; - } - - long currentTime = System.currentTimeMillis(); - - advancedCheckLock.readLock().lock(); - try { - if (cachedAdvancedCheckResult != null && - (currentTime - lastAdvancedCheckTime) < ADVANCED_CHECKS_THROTTLE_SECONDS * 1000) { - return cachedAdvancedCheckResult.isDegraded; - } - } finally { - advancedCheckLock.readLock().unlock(); - } - - // Only one thread may submit; others fall back to the (stale) cache - if (!advancedCheckInProgress.compareAndSet(false, true)) { - advancedCheckLock.readLock().lock(); - try { - return cachedAdvancedCheckResult != null && cachedAdvancedCheckResult.isDegraded; - } finally { - advancedCheckLock.readLock().unlock(); - } - } - - try { - // Perform DB I/O outside the write lock to avoid lock contention - AdvancedCheckResult result; - try (Connection connection = dataSource.getConnection()) { - result = performAdvancedMySQLChecks(connection); - } catch (Exception e) { - if (e.getCause() instanceof InterruptedException) { - Thread.currentThread().interrupt(); + if (rs.next()) { + int lockCount = rs.getInt("cnt"); + if (lockCount >= LONG_TXN_WARNING_THRESHOLD) { + logger.warn( + "[{}] InnoDB long-running transaction(s) detected | count={} | thresholdSeconds={}", + LOG_EVENT_LOCK_WAIT, lockCount, LONG_TXN_SECONDS + ); + // Graduated escalation: WARNING for 1-4, CRITICAL for 5+ + return lockCount >= LONG_TXN_CRITICAL_THRESHOLD + ? SEVERITY_CRITICAL : SEVERITY_WARNING; } - logger.debug("Failed to get connection for advanced checks: {}", e.getMessage()); - result = new AdvancedCheckResult(false); - } - - // Re-acquire write lock only to update the cache atomically - advancedCheckLock.writeLock().lock(); - try { - lastAdvancedCheckTime = currentTime; - cachedAdvancedCheckResult = result; - return result.isDegraded; - } finally { - advancedCheckLock.writeLock().unlock(); } - } finally { - advancedCheckInProgress.set(false); + } catch (Exception e) { + logger.error("[MYSQL_DIAGNOSTIC_ERROR] Long transaction check failed | error=\"{}\"", + e.getMessage()); } + return SEVERITY_OK; } - private AdvancedCheckResult performAdvancedMySQLChecks(Connection connection) { - try { - boolean hasIssues = false; - - if (hasLockWaits(connection)) { - logger.warn(DIAGNOSTIC_LOG_TEMPLATE, DIAGNOSTIC_LOCK_WAIT); - hasIssues = true; - } + private String performDeadlockCheck(Connection conn) { + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SHOW STATUS LIKE 'Innodb_deadlocks'")) { - - if (hasSlowQueries(connection)) { - logger.warn(DIAGNOSTIC_LOG_TEMPLATE, DIAGNOSTIC_SLOW_QUERIES); - hasIssues = true; - } - - if (hasConnectionPoolExhaustion()) { - logger.warn(DIAGNOSTIC_LOG_TEMPLATE, DIAGNOSTIC_POOL_EXHAUSTED); - hasIssues = true; + if (rs.next()) { + long currentDeadlocks = rs.getLong(STATUS_VALUE); + long previousDeadlocks = previousDeadlockCount.getAndSet(currentDeadlocks); + + if (currentDeadlocks > previousDeadlocks) { + long deltaDeadlocks = currentDeadlocks - previousDeadlocks; + logger.warn( + "[{}] InnoDB deadlocks detected since last run | deltaCount={} | cumulativeCount={}", + LOG_EVENT_DEADLOCK, deltaDeadlocks, currentDeadlocks + ); + return SEVERITY_WARNING; + } } - - return new AdvancedCheckResult(hasIssues); } catch (Exception e) { - logger.debug("Advanced MySQL checks encountered exception, marking degraded"); - return new AdvancedCheckResult(true); + logger.error("[MYSQL_DIAGNOSTIC_ERROR] Deadlock check failed | error=\"{}\"", + e.getMessage()); } + return SEVERITY_OK; } - private boolean hasLockWaits(Connection connection) { - try (PreparedStatement stmt = connection.prepareStatement( - "SELECT COUNT(*) FROM INFORMATION_SCHEMA.PROCESSLIST " + - "WHERE (state = 'Waiting for table metadata lock' " + - " OR state = 'Waiting for row lock' " + - " OR state = 'Waiting for lock') " + - "AND user = SUBSTRING_INDEX(USER(), '@', 1)")) { - stmt.setQueryTimeout(2); - try (ResultSet rs = stmt.executeQuery()) { - if (rs.next()) { - int lockCount = rs.getInt(1); - return lockCount > 0; + private String performSlowQueryCheck(Connection conn) { + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SHOW STATUS LIKE 'Slow_queries'")) { + + if (rs.next()) { + long slowQueries = rs.getLong(STATUS_VALUE); + long previousSlow = previousSlowQueryCount.getAndSet(slowQueries); + + // Only warn if slow queries have *increased* since last run + if (slowQueries > previousSlow) { + long delta = slowQueries - previousSlow; + logger.warn( + "[{}] New slow queries detected since last run | deltaCount={} | cumulativeCount={}", + LOG_EVENT_SLOW_QUERIES, delta, slowQueries + ); + return SEVERITY_WARNING; } } } catch (Exception e) { - logger.debug("Could not check for lock waits"); + logger.error("[MYSQL_DIAGNOSTIC_ERROR] Slow query check failed | error=\"{}\"", + e.getMessage()); } - return false; + return SEVERITY_OK; } - private boolean hasSlowQueries(Connection connection) { - try (PreparedStatement stmt = connection.prepareStatement( - "SELECT COUNT(*) FROM INFORMATION_SCHEMA.PROCESSLIST " + - "WHERE command != 'Sleep' AND time > ? AND user = SUBSTRING_INDEX(USER(), '@', 1)")) { - stmt.setQueryTimeout(2); - stmt.setInt(1, 10); - try (ResultSet rs = stmt.executeQuery()) { - if (rs.next()) { - int slowQueryCount = rs.getInt(1); - return slowQueryCount > 3; - } + private String performConnectionUsageCheck(Connection conn) { + try (Statement stmt = conn.createStatement()) { + int threadsConnected = 0; + int maxConnections = 0; + + try (ResultSet rs = stmt.executeQuery("SHOW STATUS LIKE 'Threads_connected'")) { + if (rs.next()) threadsConnected = rs.getInt(STATUS_VALUE); } - } catch (Exception e) { - logger.debug("Could not check for slow queries"); - } - return false; - } - private boolean hasConnectionPoolExhaustion() { - if (dataSource instanceof HikariDataSource hikariDataSource) { - try { - HikariPoolMXBean poolMXBean = hikariDataSource.getHikariPoolMXBean(); - - if (poolMXBean != null) { - int activeConnections = poolMXBean.getActiveConnections(); - int maxPoolSize = hikariDataSource.getMaximumPoolSize(); - - int threshold = (int) (maxPoolSize * 0.8); - return activeConnections > threshold; - } - } catch (Exception e) { - logger.debug("Could not retrieve HikariCP pool metrics"); + try (ResultSet rs = stmt.executeQuery("SHOW VARIABLES LIKE 'max_connections'")) { + if (rs.next()) maxConnections = rs.getInt(STATUS_VALUE); } - } - - return checkPoolMetricsViaJMX(); - } - private boolean checkPoolMetricsViaJMX() { - try { - MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer(); - ObjectName objectName = new ObjectName("com.zaxxer.hikari:type=Pool (*)"); - var mBeans = mBeanServer.queryMBeans(objectName, null); - - for (var mBean : mBeans) { - if (evaluatePoolMetrics(mBeanServer, mBean.getObjectName())) { - return true; + if (maxConnections > 0) { + int usagePct = (int) ((threadsConnected * 100.0) / maxConnections); + + if (usagePct >= CONNECTION_USAGE_CRITICAL) { + logger.error( + "[{}] MySQL connection pool near exhaustion | threadsConnected={} | maxConnections={} | usagePercent={}", + LOG_EVENT_POOL_EXHAUSTED, threadsConnected, maxConnections, usagePct + ); + return SEVERITY_CRITICAL; + + } else if (usagePct > CONNECTION_USAGE_WARNING) { + logger.warn( + "[{}] MySQL connection usage is high | threadsConnected={} | maxConnections={} | usagePercent={}", + LOG_EVENT_CONN_USAGE, threadsConnected, maxConnections, usagePct + ); + return SEVERITY_WARNING; } } } catch (Exception e) { - logger.debug("Could not access HikariCP pool metrics via JMX"); + logger.error("[MYSQL_DIAGNOSTIC_ERROR] Connection usage check failed | error=\"{}\"", + e.getMessage()); } - - logger.debug("Pool exhaustion check disabled: HikariCP metrics unavailable"); - return false; + return SEVERITY_OK; } - - private boolean evaluatePoolMetrics(MBeanServer mBeanServer, ObjectName objectName) { - try { - Integer activeConnections = (Integer) mBeanServer.getAttribute(objectName, "ActiveConnections"); - Integer maximumPoolSize = (Integer) mBeanServer.getAttribute(objectName, "MaximumPoolSize"); - - if (activeConnections != null && maximumPoolSize != null) { - int threshold = (int) (maximumPoolSize * 0.8); - return activeConnections > threshold; - } - } catch (Exception e) { - // Continue to next MBean - } - return false; + private String resolveDatabaseStatus(String severity) { + return switch (severity) { + case SEVERITY_CRITICAL -> STATUS_DOWN; + case SEVERITY_WARNING -> STATUS_DEGRADED; + default -> STATUS_UP; + }; } - - private static class AdvancedCheckResult { - final boolean isDegraded; - - AdvancedCheckResult(boolean isDegraded) { - this.isDegraded = isDegraded; - } + private String escalate(String current, String candidate) { + return severityRank(candidate) > severityRank(current) ? candidate : current; } - private static class HealthCheckResult { - final boolean isHealthy; - final String error; - final boolean isDegraded; - - HealthCheckResult(boolean isHealthy, String error, boolean isDegraded) { - this.isHealthy = isHealthy; - this.error = error; - this.isDegraded = isDegraded; - } + private int severityRank(String severity) { + return switch (severity) { + case SEVERITY_CRITICAL -> 2; + case SEVERITY_WARNING -> 1; + default -> 0; + }; } -} +} \ No newline at end of file diff --git a/src/main/java/com/iemr/admin/service/version/VersionService.java b/src/main/java/com/iemr/admin/service/version/VersionService.java deleted file mode 100644 index f46ce4d..0000000 --- a/src/main/java/com/iemr/admin/service/version/VersionService.java +++ /dev/null @@ -1,170 +0,0 @@ -/* -* AMRIT – Accessible Medical Records via Integrated Technology -* Integrated EHR (Electronic Health Records) Solution -* -* Copyright (C) "Piramal Swasthya Management and Research Institute" -* -* This file is part of AMRIT. -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ -package com.iemr.admin.service.version; - -import java.io.IOException; -import java.io.InputStream; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Properties; -import java.util.TimeZone; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; - -import com.fasterxml.jackson.databind.ObjectMapper; - -@Service -public class VersionService { - - private static final Logger logger = LoggerFactory.getLogger(VersionService.class); - - @Value("${app.version:unknown}") - private String appVersion; - - @Value("${maven.properties.path:META-INF/maven/com.iemr.admin/admin-api/pom.properties}") - private String mavenPropertiesPath; - - private final ObjectMapper objectMapper = new ObjectMapper(); - - /** - * Get version information as a Map for direct JSON serialization - * @return Map containing version information - */ - public Map getVersionInfo() { - return buildVersionInfo(); - } - - /** - * Get version information as JSON string (deprecated - use getVersionInfo() instead) - * @return JSON string containing version information - */ - @Deprecated - public String getVersionInformation() { - try { - Map versionInfo = buildVersionInfo(); - return objectMapper.writeValueAsString(versionInfo); - } catch (Exception e) { - logger.error("Error building version information", e); - return createErrorResponse(); - } - } - - private Map buildVersionInfo() { - Map versionInfo = new LinkedHashMap<>(); - - // Add Git information - addGitInformation(versionInfo); - - // Add build information - addBuildInformation(versionInfo); - - // Add current time - versionInfo.put("current.time", getCurrentIstTimeFormatted()); - - return versionInfo; - } - - private void addGitInformation(Map versionInfo) { - Properties gitProps = loadPropertiesFile("git.properties"); - if (gitProps != null) { - String commitId = gitProps.getProperty("git.commit.id", - gitProps.getProperty("git.commit.id.abbrev", "unknown")); - versionInfo.put("git.commit.id", commitId); - - String buildTime = gitProps.getProperty("git.build.time", - gitProps.getProperty("git.commit.time", - gitProps.getProperty("git.commit.timestamp", "unknown"))); - versionInfo.put("git.build.time", buildTime); - } else { - logger.warn("git.properties file not found. Git information will be unavailable."); - versionInfo.put("git.commit.id", "information unavailable"); - versionInfo.put("git.build.time", "information unavailable"); - } - } - - private void addBuildInformation(Map versionInfo) { - Properties buildProps = loadPropertiesFile("META-INF/build-info.properties"); - if (buildProps != null) { - String version = buildProps.getProperty("build.version", - buildProps.getProperty("build.version.number", - buildProps.getProperty("version", appVersion))); - versionInfo.put("build.version", version); - - String time = buildProps.getProperty("build.time", - buildProps.getProperty("build.timestamp", - buildProps.getProperty("timestamp", getCurrentIstTimeFormatted()))); - versionInfo.put("build.time", time); - } else { - logger.info("build-info.properties not found, trying Maven properties"); - Properties mavenProps = loadPropertiesFile(mavenPropertiesPath); - if (mavenProps != null) { - String version = mavenProps.getProperty("version", appVersion); - versionInfo.put("build.version", version); - versionInfo.put("build.time", getCurrentIstTimeFormatted()); - } else { - logger.warn("Neither build-info.properties nor Maven properties found."); - versionInfo.put("build.version", appVersion); - versionInfo.put("build.time", getCurrentIstTimeFormatted()); - } - } - } - - private String getCurrentIstTimeFormatted() { - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - sdf.setTimeZone(TimeZone.getTimeZone("Asia/Kolkata")); - return sdf.format(new Date()); - } - - private Properties loadPropertiesFile(String resourceName) { - ClassLoader classLoader = getClass().getClassLoader(); - try (InputStream inputStream = classLoader.getResourceAsStream(resourceName)) { - if (inputStream != null) { - Properties props = new Properties(); - props.load(inputStream); - return props; - } - } catch (IOException e) { - logger.warn("Could not load properties file: " + resourceName, e); - } - return null; - } - - private String createErrorResponse() { - try { - Map errorInfo = new LinkedHashMap<>(); - errorInfo.put("git.commit.id", "error retrieving information"); - errorInfo.put("git.build.time", "error retrieving information"); - errorInfo.put("build.version", appVersion); - errorInfo.put("build.time", getCurrentIstTimeFormatted()); - errorInfo.put("current.time", getCurrentIstTimeFormatted()); - return objectMapper.writeValueAsString(errorInfo); - } catch (Exception e) { - logger.error("Error creating error response", e); - return "{\"error\": \"Unable to retrieve version information\"}"; - } - } -} From 235e63eb816637a16e8834772d7b2dc676547c76 Mon Sep 17 00:00:00 2001 From: Vishwanath Balkur <118195001+vishwab1@users.noreply.github.com> Date: Tue, 28 Jul 2026 20:11:59 +0530 Subject: [PATCH 03/10] Vb/release 3.8.2 merge (#139) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Bulk registration * Bulk registration * Bulk registration * add userName and password in Bulk registration * add userName and password in Bulk registration * remove unwanted line * fix code * fix code * fix code * fix code * fix code * fix code * fix code * fix code * fix code * fix code * fix code * fix code * fix code * Httpheader content disposition changed * Coderabbitai comments adrressed * Httpheader content disposition changed (#100) * Httpheader content disposition changed * Coderabbitai comments adrressed * Compile error resolved * fix code * Main branch changes missed (#102) * Feature/signaturerelease (#103) * Main branch changes missed * Signature file changed * Feature/signaturerelease (#104) * Main branch changes missed * Signature file changed * Created new endpoint for Active and DeActive Employee Signature * coderabbit comments addressed * Update pom.xml * Cherry-pick the commits related to 3.5.0 form 3.6.0 (#106) * Compile error resolved * fix: cherry pic the #101 pr * fix: cherry pic the #101 pr * Main branch changes missed * fix: cherry pic the #103 pr * fix: cherry pic the #104 pr * fix: cherry pic the #104 pr * fix: cherry pick the #104 pr * fix code rabbit comments --------- Co-authored-by: Ravi Shanigarapu * fix:casesheet signature * fix:pom file change * API changes in Signature enhancement for Casesheet (#107) * fix:casesheet signature * fix:pom file change * fix: pom version * fix code * fix code * fix code * fix code * fix code * fix code * fix code * fix code * fix code * fix code * fix code * fix code * fix code * fix: amm-1927 send headers only if the request is from the allowed origin * fix: amm-1927 coderabbit fixes * Update regex handling for localhost URLs * Enhance regex pattern for URL matching * fix code * fix code * fix vulnerabilitie code * fix security hotspots * fixed conflicts * Add facility hierarchy creation with village and parent-child mapping (#121) * fix:changed the pom xml * fix: added facilty type master change * feat: created facility creation * fix: rabiit review fix * fix: rabiit review fix * fix: rabiit review fix * fix: pom version * fix: facility hierarchy and facility type management (#125) * fix:changed the pom xml * fix: added facilty type master change * feat: created facility creation * feat:added work location * feat:added work location * fix: rabiit review fix * fix: rabiit review fix * fix: rabiit review fix * fix: ui chnges * fix: pom version * fix: corrections * fix: facilty hierachy * fix: facility heirachy * fix: remove logs folder from repository Co-Authored-By: Claude Opus 4.6 (1M context) * fix: add logs/ to .gitignore Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) * Facility hierarchy inventory mapping and store field management (#128) * fix:changed the pom xml * fix: added facilty type master change * feat: created facility creation * feat:added work location * feat:added work location * fix: rabiit review fix * fix: rabiit review fix * fix: rabiit review fix * fix: ui chnges * fix: pom version * fix: corrections * fix: facilty hierachy * fix: facility heirachy * fix: item facility mapping and store updates Co-Authored-By: Claude Opus 4.6 (1M context) * fix: fixed inventory flow --------- Co-authored-by: Claude Opus 4.6 (1M context) * merge 3.6.2 to release 3.8.1 (#132) * Cherry-pick health and version API enhancements to release-3.6.1 (#124) * feat(health,version): add health and version endponts * fix(health): add constant and remove duplicates * fix(health): avoid permanent DEGRADED from historical deadlocks * fix(health): Removed the unnecessary boolean literal * fix(health): Fixed the broken lock-wait detection * fix(health): avoid blocking DB I/O under write lock and restore interrupt flag * fix(health): add cancelFutures in healthservice * fix(health): close basic DB connection before advanced checks and remove shared-map race * feat: expose account lock state in SearchEmployee4 (#129) Co-authored-by: Varun Deep Saini * Fix the reset password issue (#131) * fix: reset password * fix: build issue * fix: update the url * fix: update config properties * fix: initialize constructor --------- Co-authored-by: KOPPIREDDY DURGA PRASAD <144464542+DurgaPrasad-54@users.noreply.github.com> Co-authored-by: Varun Deep Saini Co-authored-by: Varun Deep Saini Co-authored-by: Vanitha S <116701245+vanitha1822@users.noreply.github.com> * fix: avoid ONLY_FULL_GROUP_BY violation in service lookup queries GROUP BY sm.serviceName selected non-aggregated, non-functionally-dependent columns (serviceID, isNational, statusID), which MySQL rejects under ONLY_FULL_GROUP_BY. Use SELECT DISTINCT instead since no aggregation is actually needed. * fix: bulk employee registration sets username from contact number instead of username field mUser.setUserName(employee.getContactNo()) was using the uploaded contact number as the login username, ignoring the actual UserName column in the bulk upload sheet. Co-Authored-By: Claude Sonnet 4.6 * Add Nikshay location master for Stop TB (TU/Facility) and complete duplicate-mapping fix - New isolated tables/entities: NikshayTU, NikshayFacility, NikshayVillageFacilityMapping, with read-only cascading lookup endpoints (NikshayLocationController) — no existing master tables touched. - M_UserServiceRoleMapping2 gains NikshayTUID/NikshayFacilityID (nullable, additive), persisted on both create and update save paths. - Complete the pre-existing softDeleteOldMappings fix by adding the missing service interface/impl methods the controller already called. Co-Authored-By: Claude Sonnet 5 * feat(nikshay): replace AMRIT-village-matching with independent Nikshay hierarchy Nikshay's TU/Facility/Village hierarchy was previously matched against AMRIT's existing state/district/block/village masters by name. That approach left ~44% of villages unmatched in practice — partly from incomplete migration runs, partly from real staleness in AMRIT's own district/block data (e.g. Andhra Pradesh's 2022 district reorganization never propagated to m_districtblock/m_DistrictBranchMapping). Replace it with a fully self-contained Nikshay hierarchy (NikshayState, NikshayDistrict, NikshayTU, NikshayFacility, NikshayVillage) sourced directly from Nikshay's own imported data, with no AMRIT matching involved. Verified: 100% village coverage (325,426/325,426) vs the previous approach's ~56%. NikshayLocationController's endpoints keep the same paths and query params, now backed by the new tables. * fix(nikshay): store NikshayTUID/NikshayFacilityID as comma-joined String Admin-UI now saves one row per user-role with a comma-joined list of TU/Facility IDs instead of one row per TU x Facility combination, so the DB columns became TEXT. Update the entity and the request-body DTO (Previleges1097_3) to match — both were still Integer, which would have failed to deserialize a value like "12,45,78". Co-Authored-By: Claude Sonnet 5 * feat(nikshay): add endpoint to read back saved TU/Facility/District for Edit v_userservicerolemapping (and its entity, what the Admin-UI's mapped-list screen reads) never exposes DistrictID/NikshayTUID/NikshayFacilityID, only legacy AMRIT WorkingDistrictID/WorkingDistrictName - always null for Stop TB, which never populates WorkingLocationID. Edit had no way to read back what was saved, so the TU/Facility/Village pickers always loaded empty. Reads m_userservicerolemapping directly by USRMappingID (reusing the existing EmployeeMasterRepo.findByUSRMappingID), same pattern as the FLW-API worklist-scope query - additive only, the shared view and every other service line reading it stay untouched. Co-Authored-By: Claude Sonnet 5 * fix: add missing @Transactional to softDeleteOldMappings InvalidDataAccessApiUsageException: "Executing an update/delete query" - a @Modifying UPDATE query with no surrounding transaction. Pre-existing bug in shared code (updateUserRoleMapping, used by every service line), dormant until now: Stop TB's Edit form was always invalid before today's fixes (Update button permanently disabled), so nothing ever reached this code path through Stop TB specifically. Same pattern the other @Modifying query in this file already follows correctly. Co-Authored-By: Claude Sonnet 5 * fix(nikshay): stop patching District with AMRIT lookup for Stop TB getMappedRole()'s null-district patch (getDirectStateDistrictByMappingIDs) joins the raw DistrictID column against AMRIT's m_district - correct for HWC/FLW, where that column genuinely holds an AMRIT district ID. Stop TB's DistrictID holds a Nikshay district ID instead, so the same join resolves to whatever AMRIT district happens to share that numeric ID by coincidence (e.g. Nikshay's Angul, ID 292, resolving to AMRIT's Uttar Kannad, also ID 292) - showing a completely wrong, unrelated district name in the mapped-users list. Skip the District patch for Stop TB rows specifically; State/Block patching is untouched since those values are genuinely AMRIT-sourced even for Stop TB. Co-Authored-By: Claude Sonnet 5 * Add Nikshay location master for Stop TB (TU/Facility) and complete duplicate-mapping fix - New isolated tables/entities: NikshayTU, NikshayFacility, NikshayVillageFacilityMapping, with read-only cascading lookup endpoints (NikshayLocationController) — no existing master tables touched. - M_UserServiceRoleMapping2 gains NikshayTUID/NikshayFacilityID (nullable, additive), persisted on both create and update save paths. - Complete the pre-existing softDeleteOldMappings fix by adding the missing service interface/impl methods the controller already called. Co-Authored-By: Claude Sonnet 5 --------- Co-authored-by: Saurav Mishra Co-authored-by: Sushant <77480199+sushant-bizbrolly@users.noreply.github.com> Co-authored-by: Saurav Mishra <80103738+SauravBizbRolly@users.noreply.github.com> Co-authored-by: Mithun James Co-authored-by: Ravi Shanigarapu Co-authored-by: ravishanigarapu <133210792+ravishanigarapu@users.noreply.github.com> Co-authored-by: Amoghavarsh <93114621+5Amogh@users.noreply.github.com> Co-authored-by: SnehaRH <77656297+snehar-nd@users.noreply.github.com> Co-authored-by: 5Amogh Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: KOPPIREDDY DURGA PRASAD <144464542+DurgaPrasad-54@users.noreply.github.com> Co-authored-by: Varun Deep Saini Co-authored-by: Varun Deep Saini Co-authored-by: Vanitha S <116701245+vanitha1822@users.noreply.github.com> --- .../EmployeeMasterController.java | 11 + .../nikshay/NikshayLocationController.java | 193 ++++++++++++++++++ .../M_UserServiceRoleMapping2.java | 12 ++ .../admin/data/nikshay/NikshayDistrict.java | 95 +++++++++ .../admin/data/nikshay/NikshayFacility.java | 98 +++++++++ .../iemr/admin/data/nikshay/NikshayState.java | 90 ++++++++ .../iemr/admin/data/nikshay/NikshayTU.java | 94 +++++++++ .../admin/data/nikshay/NikshayVillage.java | 93 +++++++++ .../NikshayVillageFacilityMapping.java | 83 ++++++++ .../MProviderservicemappingBlockingRepo.java | 2 +- .../employeemaster/EmployeeMasterRepo.java | 6 + .../repo/nikshay/NikshayDistrictRepo.java | 38 ++++ .../repo/nikshay/NikshayFacilityRepo.java | 41 ++++ .../admin/repo/nikshay/NikshayStateRepo.java | 37 ++++ .../admin/repo/nikshay/NikshayTURepo.java | 41 ++++ .../NikshayVillageFacilityMappingRepo.java | 42 ++++ .../repo/nikshay/NikshayVillageRepo.java | 38 ++++ ...cerolemappingForRoleProviderAdminRepo.java | 5 +- .../BulkRegistrationServiceImpl.java | 2 +- .../employeemaster/EmployeeMasterInter.java | 3 + .../EmployeeMasterServiceImpl.java | 18 +- .../EmployeeSignatureServiceImpl.java | 1 + .../to/employeemaster/Previleges1097_3.java | 16 ++ .../utils/JwtUserIdValidationFilter.java | 1 - 24 files changed, 1052 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/iemr/admin/controller/nikshay/NikshayLocationController.java create mode 100644 src/main/java/com/iemr/admin/data/nikshay/NikshayDistrict.java create mode 100644 src/main/java/com/iemr/admin/data/nikshay/NikshayFacility.java create mode 100644 src/main/java/com/iemr/admin/data/nikshay/NikshayState.java create mode 100644 src/main/java/com/iemr/admin/data/nikshay/NikshayTU.java create mode 100644 src/main/java/com/iemr/admin/data/nikshay/NikshayVillage.java create mode 100644 src/main/java/com/iemr/admin/data/nikshay/NikshayVillageFacilityMapping.java create mode 100644 src/main/java/com/iemr/admin/repo/nikshay/NikshayDistrictRepo.java create mode 100644 src/main/java/com/iemr/admin/repo/nikshay/NikshayFacilityRepo.java create mode 100644 src/main/java/com/iemr/admin/repo/nikshay/NikshayStateRepo.java create mode 100644 src/main/java/com/iemr/admin/repo/nikshay/NikshayTURepo.java create mode 100644 src/main/java/com/iemr/admin/repo/nikshay/NikshayVillageFacilityMappingRepo.java create mode 100644 src/main/java/com/iemr/admin/repo/nikshay/NikshayVillageRepo.java diff --git a/src/main/java/com/iemr/admin/controller/employeemaster/EmployeeMasterController.java b/src/main/java/com/iemr/admin/controller/employeemaster/EmployeeMasterController.java index 35b6ed6..33d980c 100644 --- a/src/main/java/com/iemr/admin/controller/employeemaster/EmployeeMasterController.java +++ b/src/main/java/com/iemr/admin/controller/employeemaster/EmployeeMasterController.java @@ -1813,6 +1813,8 @@ public String UserRoleMappings(@RequestBody String userRoleMapping, HttpServletR resDataMap1.setVillageID(previl.getVillageID()); resDataMap1.setVillageName(previl.getVillageName()); resDataMap1.setFacilityID(previl.getFacilityID()); + resDataMap1.setNikshayTUID(previl.getNikshayTUID()); + resDataMap1.setNikshayFacilityID(previl.getNikshayFacilityID()); resList1.add(resDataMap1); } @@ -1861,6 +1863,13 @@ public String updateUserRoleMapping(@RequestBody String updateUserRoleMapping, H } } + // Soft-delete other active mappings for same user+service to prevent duplicates + // (e.g. old roleID 122 rows left over when new roleID 128 mapping was created) + if (pre.getUserID() != null && pre.getProviderServiceMapID() != null && pre.getuSRMappingID() != null) { + employeeMasterInter.softDeleteOldMappings( + pre.getUserID(), pre.getProviderServiceMapID(), pre.getuSRMappingID()); + } + usrRole.setUserID(pre.getUserID()); usrRole.setRoleID(pre.getRoleID()); usrRole.setAgentPassword(pre.getAgentPassword()); @@ -1874,6 +1883,8 @@ public String updateUserRoleMapping(@RequestBody String updateUserRoleMapping, H usrRole.setVillageID(pre.getVillageID()); usrRole.setVillageName(pre.getVillageName()); usrRole.setFacilityID(pre.getFacilityID()); + usrRole.setNikshayTUID(pre.getNikshayTUID()); + usrRole.setNikshayFacilityID(pre.getNikshayFacilityID()); if (pre.getTeleConsultation() != null) { usrRole.setTeleConsultation(pre.getTeleConsultation()); diff --git a/src/main/java/com/iemr/admin/controller/nikshay/NikshayLocationController.java b/src/main/java/com/iemr/admin/controller/nikshay/NikshayLocationController.java new file mode 100644 index 0000000..aa8d0a8 --- /dev/null +++ b/src/main/java/com/iemr/admin/controller/nikshay/NikshayLocationController.java @@ -0,0 +1,193 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.admin.controller.nikshay; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.google.gson.Gson; +import com.iemr.admin.data.employeemaster.M_UserServiceRoleMapping2; +import com.iemr.admin.data.nikshay.NikshayDistrict; +import com.iemr.admin.data.nikshay.NikshayFacility; +import com.iemr.admin.data.nikshay.NikshayState; +import com.iemr.admin.data.nikshay.NikshayTU; +import com.iemr.admin.data.nikshay.NikshayVillage; +import com.iemr.admin.repo.employeemaster.EmployeeMasterRepo; +import com.iemr.admin.repo.nikshay.NikshayDistrictRepo; +import com.iemr.admin.repo.nikshay.NikshayFacilityRepo; +import com.iemr.admin.repo.nikshay.NikshayStateRepo; +import com.iemr.admin.repo.nikshay.NikshayTURepo; +import com.iemr.admin.repo.nikshay.NikshayVillageRepo; +import com.iemr.admin.utils.response.OutputResponse; + +import io.swagger.v3.oas.annotations.Operation; + +/** + * Read-only cascading lookups for Stop TB's Nikshay location hierarchy: + * State -> District -> TU -> Facility -> Village — all sourced from + * Nikshay's own imported data (m_nikshay_*), independent of AMRIT's own + * state/district/village masters. Earlier versions matched against AMRIT's + * existing location tables by name, but AMRIT's own district/block data has + * real staleness (e.g. post-2022 Andhra Pradesh district reorganization + * never propagated), which left a large fraction of villages unmatched. + * This hierarchy avoids that entirely. + * + * Every endpoint here only reads existing tables — nothing is inserted or + * altered from these calls. + */ +@RestController +public class NikshayLocationController { + + private final Logger logger = LoggerFactory.getLogger(NikshayLocationController.class); + + @Autowired + private NikshayStateRepo nikshayStateRepo; + + @Autowired + private EmployeeMasterRepo employeeMasterRepo; + + @Autowired + private NikshayDistrictRepo nikshayDistrictRepo; + + @Autowired + private NikshayTURepo nikshayTURepo; + + @Autowired + private NikshayFacilityRepo nikshayFacilityRepo; + + @Autowired + private NikshayVillageRepo nikshayVillageRepo; + + @Operation(summary = "Get all Nikshay states") + @GetMapping(value = "/nikshay/location/states", produces = "application/json") + public String getStates() { + OutputResponse response = new OutputResponse(); + try { + List states = nikshayStateRepo.findAllActive(); + response.setResponse(states.toString()); + } catch (Exception e) { + logger.error("Error fetching Nikshay states: " + e.getMessage(), e); + response.setError(e); + } + return response.toString(); + } + + @Operation(summary = "Get Nikshay districts for a Nikshay state") + @GetMapping(value = "/nikshay/location/districts", produces = "application/json") + public String getDistricts(@RequestParam("stateID") Integer stateID) { + OutputResponse response = new OutputResponse(); + try { + List districts = nikshayDistrictRepo.findByStateID(stateID); + response.setResponse(districts.toString()); + } catch (Exception e) { + logger.error("Error fetching districts for stateID " + stateID + ": " + e.getMessage(), e); + response.setError(e); + } + return response.toString(); + } + + @Operation(summary = "Get Nikshay TUs for a district") + @GetMapping(value = "/nikshay/location/tus", produces = "application/json") + public String getTUs(@RequestParam("districtID") Integer districtID) { + OutputResponse response = new OutputResponse(); + try { + List tus = nikshayTURepo.findByDistrictID(districtID); + response.setResponse(tus.toString()); + } catch (Exception e) { + logger.error("Error fetching Nikshay TUs for districtID " + districtID + ": " + e.getMessage(), e); + response.setError(e); + } + return response.toString(); + } + + @Operation(summary = "Get Nikshay facilities for one or more TUs (comma-separated tuIDs)") + @GetMapping(value = "/nikshay/location/facilities", produces = "application/json") + public String getFacilities(@RequestParam("tuIDs") String tuIDs) { + OutputResponse response = new OutputResponse(); + try { + List ids = parseIntCsv(tuIDs); + List facilities = nikshayFacilityRepo.findByTUIDs(ids); + response.setResponse(facilities.toString()); + } catch (Exception e) { + logger.error("Error fetching Nikshay facilities for tuIDs " + tuIDs + ": " + e.getMessage(), e); + response.setError(e); + } + return response.toString(); + } + + @Operation(summary = "Get Nikshay villages for one or more Nikshay facilities (comma-separated facilityIDs)") + @GetMapping(value = "/nikshay/location/villages", produces = "application/json") + public String getVillages(@RequestParam("facilityIDs") String facilityIDs) { + OutputResponse response = new OutputResponse(); + try { + List ids = parseIntCsv(facilityIDs); + List villages = nikshayVillageRepo.findByFacilityIDs(ids); + response.setResponse(villages.toString()); + } catch (Exception e) { + logger.error("Error fetching villages for facilityIDs " + facilityIDs + ": " + e.getMessage(), e); + response.setError(e); + } + return response.toString(); + } + + @Operation(summary = "Get the Nikshay DistrictID/TUID/FacilityID saved on a Stop TB " + + "user-role mapping row, by USRMappingID. Reads m_userservicerolemapping " + + "directly (NOT the shared v_userservicerolemapping view, which does not " + + "expose these Stop TB-only columns), so the view and every other service " + + "line reading it are untouched.") + @GetMapping(value = "/nikshay/location/userMapping", produces = "application/json") + public String getUserMappingNikshayData(@RequestParam("usrMappingID") Integer usrMappingID) { + OutputResponse response = new OutputResponse(); + try { + M_UserServiceRoleMapping2 row = employeeMasterRepo.findByUSRMappingID(usrMappingID); + Map result = new HashMap<>(); + if (row != null) { + result.put("districtID", row.getDistrictID()); + result.put("nikshayTUID", row.getNikshayTUID()); + result.put("nikshayFacilityID", row.getNikshayFacilityID()); + } + response.setResponse(new Gson().toJson(result)); + } catch (Exception e) { + logger.error("Error fetching Nikshay data for usrMappingID " + usrMappingID + ": " + e.getMessage(), e); + response.setError(e); + } + return response.toString(); + } + + private List parseIntCsv(String csv) { + return Arrays.stream(csv.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(Integer::parseInt) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/iemr/admin/data/employeemaster/M_UserServiceRoleMapping2.java b/src/main/java/com/iemr/admin/data/employeemaster/M_UserServiceRoleMapping2.java index ad6575a..e33da77 100644 --- a/src/main/java/com/iemr/admin/data/employeemaster/M_UserServiceRoleMapping2.java +++ b/src/main/java/com/iemr/admin/data/employeemaster/M_UserServiceRoleMapping2.java @@ -423,6 +423,18 @@ public class M_UserServiceRoleMapping2 { @Column(name = "isOutbound") private Boolean outbound; + // Stop TB / Nikshay location scope — populated only when serviceName = "Stop TB". + // Comma-joined list of Nikshay TU/Facility IDs (e.g. "12,45,78") — a user + // can be mapped to several TUs/Facilities in one row, same idea as + // Villageid. + @Expose + @Column(name = "NikshayTUID") + private String nikshayTUID; + + @Expose + @Column(name = "NikshayFacilityID") + private String nikshayFacilityID; + public M_UserServiceRoleMapping2() { } diff --git a/src/main/java/com/iemr/admin/data/nikshay/NikshayDistrict.java b/src/main/java/com/iemr/admin/data/nikshay/NikshayDistrict.java new file mode 100644 index 0000000..9218944 --- /dev/null +++ b/src/main/java/com/iemr/admin/data/nikshay/NikshayDistrict.java @@ -0,0 +1,95 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.admin.data.nikshay; + +import java.sql.Timestamp; + +import com.google.gson.annotations.Expose; +import com.iemr.admin.utils.mapper.OutputMapper; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Transient; +import lombok.Data; + +/** + * Nikshay's own District master — imported directly from Nikshay's location + * data, independent of AMRIT's existing district master (m_District). Kept + * separate rather than matched to AMRIT because AMRIT's own district/block + * data has been found to be stale in places (e.g. post-2022 AP district + * reorganization never propagated), which made name-matching unreliable. + */ +@Data +@Entity +@Table(name = "m_nikshay_district") +public class NikshayDistrict { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Expose + @Column(name = "NikshayDistrictID") + private Integer nikshayDistrictID; + + @Expose + @Column(name = "NikshayCode") + private String nikshayCode; + + @Expose + @Column(name = "DistrictName") + private String districtName; + + @Expose + @Column(name = "NikshayStateID") + private Integer nikshayStateID; + + @Expose + @Column(name = "Deleted") + private Boolean deleted = false; + + @Expose + @Column(name = "CreatedBy") + private String createdBy; + + @Expose + @Column(name = "CreatedDate", insertable = false, updatable = false) + private Timestamp createdDate; + + @Expose + @Column(name = "ModifiedBy") + private String modifiedBy; + + @Expose + @Column(name = "LastModDate", insertable = false, updatable = false) + private Timestamp lastModDate; + + @Transient + private OutputMapper outputMapper = new OutputMapper(); + + @Override + public String toString() { + return outputMapper.gson().toJson(this); + } +} diff --git a/src/main/java/com/iemr/admin/data/nikshay/NikshayFacility.java b/src/main/java/com/iemr/admin/data/nikshay/NikshayFacility.java new file mode 100644 index 0000000..a383572 --- /dev/null +++ b/src/main/java/com/iemr/admin/data/nikshay/NikshayFacility.java @@ -0,0 +1,98 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.admin.data.nikshay; + +import java.sql.Timestamp; + +import com.google.gson.annotations.Expose; +import com.iemr.admin.utils.mapper.OutputMapper; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Transient; +import lombok.Data; + +/** + * Nikshay's PHC/HWC/CHC facility master. Verified against production data + * that AMRIT's existing m_facility has no meaningful overlap with Nikshay's + * facility list (0 matches out of 343,615) — kept as its own table rather + * than reusing m_facility. + */ +@Data +@Entity +@Table(name = "m_nikshay_facility") +public class NikshayFacility { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Expose + @Column(name = "NikshayFacilityID") + private Integer nikshayFacilityID; + + @Expose + @Column(name = "NikshayCode") + private String nikshayCode; + + @Expose + @Column(name = "FacilityName") + private String facilityName; + + @Expose + @Column(name = "FacilityCode") + private String facilityCode; + + @Expose + @Column(name = "NikshayTUID") + private Integer nikshayTUID; + + @Expose + @Column(name = "Deleted") + private Boolean deleted = false; + + @Expose + @Column(name = "CreatedBy") + private String createdBy; + + @Expose + @Column(name = "CreatedDate", insertable = false, updatable = false) + private Timestamp createdDate; + + @Expose + @Column(name = "ModifiedBy") + private String modifiedBy; + + @Expose + @Column(name = "LastModDate", insertable = false, updatable = false) + private Timestamp lastModDate; + + @Transient + private OutputMapper outputMapper = new OutputMapper(); + + @Override + public String toString() { + return outputMapper.gson().toJson(this); + } +} diff --git a/src/main/java/com/iemr/admin/data/nikshay/NikshayState.java b/src/main/java/com/iemr/admin/data/nikshay/NikshayState.java new file mode 100644 index 0000000..66450c3 --- /dev/null +++ b/src/main/java/com/iemr/admin/data/nikshay/NikshayState.java @@ -0,0 +1,90 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.admin.data.nikshay; + +import java.sql.Timestamp; + +import com.google.gson.annotations.Expose; +import com.iemr.admin.utils.mapper.OutputMapper; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Transient; +import lombok.Data; + +/** + * Nikshay's own State master — imported directly from Nikshay's location + * data, independent of AMRIT's existing state master. No name-matching + * against AMRIT is performed; this is the top of a fully self-contained + * Nikshay hierarchy (State -> District -> TU -> Facility -> Village). + */ +@Data +@Entity +@Table(name = "m_nikshay_state") +public class NikshayState { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Expose + @Column(name = "NikshayStateID") + private Integer nikshayStateID; + + @Expose + @Column(name = "NikshayCode") + private String nikshayCode; + + @Expose + @Column(name = "StateName") + private String stateName; + + @Expose + @Column(name = "Deleted") + private Boolean deleted = false; + + @Expose + @Column(name = "CreatedBy") + private String createdBy; + + @Expose + @Column(name = "CreatedDate", insertable = false, updatable = false) + private Timestamp createdDate; + + @Expose + @Column(name = "ModifiedBy") + private String modifiedBy; + + @Expose + @Column(name = "LastModDate", insertable = false, updatable = false) + private Timestamp lastModDate; + + @Transient + private OutputMapper outputMapper = new OutputMapper(); + + @Override + public String toString() { + return outputMapper.gson().toJson(this); + } +} diff --git a/src/main/java/com/iemr/admin/data/nikshay/NikshayTU.java b/src/main/java/com/iemr/admin/data/nikshay/NikshayTU.java new file mode 100644 index 0000000..25023b0 --- /dev/null +++ b/src/main/java/com/iemr/admin/data/nikshay/NikshayTU.java @@ -0,0 +1,94 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.admin.data.nikshay; + +import java.sql.Timestamp; + +import com.google.gson.annotations.Expose; +import com.iemr.admin.utils.mapper.OutputMapper; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Transient; +import lombok.Data; + +/** + * Nikshay's TU (Tuberculosis Unit) master — Nikshay's own sub-district + * administrative unit, referred to as "Block" on some Nikshay screens. + * No equivalent exists in AMRIT's own masters; linked to Nikshay's own + * District master (NikshayDistrict), not AMRIT's m_District. + */ +@Data +@Entity +@Table(name = "m_nikshay_tu") +public class NikshayTU { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Expose + @Column(name = "NikshayTUID") + private Integer nikshayTUID; + + @Expose + @Column(name = "NikshayCode") + private String nikshayCode; + + @Expose + @Column(name = "TUName") + private String tUName; + + @Expose + @Column(name = "NikshayDistrictID") + private Integer nikshayDistrictID; + + @Expose + @Column(name = "Deleted") + private Boolean deleted = false; + + @Expose + @Column(name = "CreatedBy") + private String createdBy; + + @Expose + @Column(name = "CreatedDate", insertable = false, updatable = false) + private Timestamp createdDate; + + @Expose + @Column(name = "ModifiedBy") + private String modifiedBy; + + @Expose + @Column(name = "LastModDate", insertable = false, updatable = false) + private Timestamp lastModDate; + + @Transient + private OutputMapper outputMapper = new OutputMapper(); + + @Override + public String toString() { + return outputMapper.gson().toJson(this); + } +} diff --git a/src/main/java/com/iemr/admin/data/nikshay/NikshayVillage.java b/src/main/java/com/iemr/admin/data/nikshay/NikshayVillage.java new file mode 100644 index 0000000..b9bfd6b --- /dev/null +++ b/src/main/java/com/iemr/admin/data/nikshay/NikshayVillage.java @@ -0,0 +1,93 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.admin.data.nikshay; + +import java.sql.Timestamp; + +import com.google.gson.annotations.Expose; +import com.iemr.admin.utils.mapper.OutputMapper; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Transient; +import lombok.Data; + +/** + * Nikshay's own Village master, linked directly to its Nikshay Facility. + * Imported straight from Nikshay's location data with no matching against + * AMRIT's village master (m_DistrictBranchMapping) — a prior approach that + * matched by name against AMRIT villages left ~44% of villages unmatched, + * partly from incomplete migration runs and partly from real staleness in + * AMRIT's own district/block data. This table guarantees full coverage of + * every village Nikshay has, independent of AMRIT's location hierarchy. + */ +@Data +@Entity +@Table(name = "m_nikshay_village") +public class NikshayVillage { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Expose + @Column(name = "NikshayVillageID") + private Integer nikshayVillageID; + + @Expose + @Column(name = "VillageName") + private String villageName; + + @Expose + @Column(name = "NikshayFacilityID") + private Integer nikshayFacilityID; + + @Expose + @Column(name = "Deleted") + private Boolean deleted = false; + + @Expose + @Column(name = "CreatedBy") + private String createdBy; + + @Expose + @Column(name = "CreatedDate", insertable = false, updatable = false) + private Timestamp createdDate; + + @Expose + @Column(name = "ModifiedBy") + private String modifiedBy; + + @Expose + @Column(name = "LastModDate", insertable = false, updatable = false) + private Timestamp lastModDate; + + @Transient + private OutputMapper outputMapper = new OutputMapper(); + + @Override + public String toString() { + return outputMapper.gson().toJson(this); + } +} diff --git a/src/main/java/com/iemr/admin/data/nikshay/NikshayVillageFacilityMapping.java b/src/main/java/com/iemr/admin/data/nikshay/NikshayVillageFacilityMapping.java new file mode 100644 index 0000000..fad7b42 --- /dev/null +++ b/src/main/java/com/iemr/admin/data/nikshay/NikshayVillageFacilityMapping.java @@ -0,0 +1,83 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.admin.data.nikshay; + +import java.sql.Timestamp; + +import com.google.gson.annotations.Expose; +import com.iemr.admin.utils.mapper.OutputMapper; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Transient; +import lombok.Data; + +/** + * Links an existing AMRIT village (m_DistrictBranchMapping.DistrictBranchID) + * to the Nikshay Facility/TU it falls under. Built once from the location + * import's name-matching results — used to trace a beneficiary's existing + * village back to their Nikshay Facility/TU without asking the field worker + * to re-select it per case. + */ +@Data +@Entity +@Table(name = "m_nikshay_village_facility_mapping") +public class NikshayVillageFacilityMapping { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Expose + @Column(name = "MappingID") + private Integer mappingID; + + @Expose + @Column(name = "AmritVillageID") + private Integer amritVillageID; + + @Expose + @Column(name = "NikshayFacilityID") + private Integer nikshayFacilityID; + + @Expose + @Column(name = "NikshayTUID") + private Integer nikshayTUID; + + @Expose + @Column(name = "CreatedBy") + private String createdBy; + + @Expose + @Column(name = "CreatedDate", insertable = false, updatable = false) + private Timestamp createdDate; + + @Transient + private OutputMapper outputMapper = new OutputMapper(); + + @Override + public String toString() { + return outputMapper.gson().toJson(this); + } +} diff --git a/src/main/java/com/iemr/admin/repo/blocking/MProviderservicemappingBlockingRepo.java b/src/main/java/com/iemr/admin/repo/blocking/MProviderservicemappingBlockingRepo.java index 1b7b082..e0a0ffe 100644 --- a/src/main/java/com/iemr/admin/repo/blocking/MProviderservicemappingBlockingRepo.java +++ b/src/main/java/com/iemr/admin/repo/blocking/MProviderservicemappingBlockingRepo.java @@ -85,7 +85,7 @@ void blockProviderByProviderIdAndServiceId(@Param("serviceProviderID") Integer s @Query(" SELECT distinct srm.providerServiceMapID,srm.serviceProviderID,srm.serviceID," + " sm.serviceName as serviceName," + " sm.isNational as isNational " + " FROM M_Providerservicemapping_Blocking srm " + " JOIN srm.m_ServicemasterForBlocking sm" - + " WHERE srm.serviceProviderID =:serviceProviderID AND srm.deleted=false" + " GROUP BY sm.serviceName") + + " WHERE srm.serviceProviderID =:serviceProviderID AND srm.deleted=false") ArrayList getServiceLiensUsingProvider(@Param("serviceProviderID") Integer serviceProviderID); @Transactional diff --git a/src/main/java/com/iemr/admin/repo/employeemaster/EmployeeMasterRepo.java b/src/main/java/com/iemr/admin/repo/employeemaster/EmployeeMasterRepo.java index 401b0c1..e2bf74f 100644 --- a/src/main/java/com/iemr/admin/repo/employeemaster/EmployeeMasterRepo.java +++ b/src/main/java/com/iemr/admin/repo/employeemaster/EmployeeMasterRepo.java @@ -155,4 +155,10 @@ boolean existsByUserIDAndRoleIDAndProviderServiceMapIDAndFacilityIDAndDeletedFal // Fix 2: count active USR rows for supervisor (check if other facilities remain) long countByUserIDAndRoleIDAndDeletedFalse(Integer userID, Integer roleID); + // Soft-delete old duplicate mappings for same user+service, excluding the current row being updated + @Transactional + @Modifying + @Query("UPDATE M_UserServiceRoleMapping2 u SET u.deleted = true WHERE u.userID = :userID AND u.providerServiceMapID = :providerServiceMapID AND u.uSRMappingID != :excludeUSRMappingID AND u.deleted = false") + int softDeleteOldMappings(@Param("userID") Integer userID, @Param("providerServiceMapID") Integer providerServiceMapID, @Param("excludeUSRMappingID") Integer excludeUSRMappingID); + } diff --git a/src/main/java/com/iemr/admin/repo/nikshay/NikshayDistrictRepo.java b/src/main/java/com/iemr/admin/repo/nikshay/NikshayDistrictRepo.java new file mode 100644 index 0000000..30b6ee9 --- /dev/null +++ b/src/main/java/com/iemr/admin/repo/nikshay/NikshayDistrictRepo.java @@ -0,0 +1,38 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.admin.repo.nikshay; + +import java.util.List; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import com.iemr.admin.data.nikshay.NikshayDistrict; + +@Repository +public interface NikshayDistrictRepo extends CrudRepository { + + @Query("select d from NikshayDistrict d where d.nikshayStateID = :stateID and d.deleted = false order by d.districtName") + List findByStateID(@Param("stateID") Integer stateID); +} diff --git a/src/main/java/com/iemr/admin/repo/nikshay/NikshayFacilityRepo.java b/src/main/java/com/iemr/admin/repo/nikshay/NikshayFacilityRepo.java new file mode 100644 index 0000000..1d5916c --- /dev/null +++ b/src/main/java/com/iemr/admin/repo/nikshay/NikshayFacilityRepo.java @@ -0,0 +1,41 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.admin.repo.nikshay; + +import java.util.List; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import com.iemr.admin.data.nikshay.NikshayFacility; + +@Repository +public interface NikshayFacilityRepo extends CrudRepository { + + @Query("select f from NikshayFacility f where f.nikshayTUID in (:tuIDs) and f.deleted = false order by f.facilityName") + List findByTUIDs(@Param("tuIDs") List tuIDs); + + @Query("select f from NikshayFacility f where f.nikshayFacilityID in (:facilityIDs) and f.deleted = false") + List findByIds(@Param("facilityIDs") List facilityIDs); +} diff --git a/src/main/java/com/iemr/admin/repo/nikshay/NikshayStateRepo.java b/src/main/java/com/iemr/admin/repo/nikshay/NikshayStateRepo.java new file mode 100644 index 0000000..7ffbf2e --- /dev/null +++ b/src/main/java/com/iemr/admin/repo/nikshay/NikshayStateRepo.java @@ -0,0 +1,37 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.admin.repo.nikshay; + +import java.util.List; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +import com.iemr.admin.data.nikshay.NikshayState; + +@Repository +public interface NikshayStateRepo extends CrudRepository { + + @Query("select s from NikshayState s where s.deleted = false order by s.stateName") + List findAllActive(); +} diff --git a/src/main/java/com/iemr/admin/repo/nikshay/NikshayTURepo.java b/src/main/java/com/iemr/admin/repo/nikshay/NikshayTURepo.java new file mode 100644 index 0000000..63b2313 --- /dev/null +++ b/src/main/java/com/iemr/admin/repo/nikshay/NikshayTURepo.java @@ -0,0 +1,41 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.admin.repo.nikshay; + +import java.util.List; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import com.iemr.admin.data.nikshay.NikshayTU; + +@Repository +public interface NikshayTURepo extends CrudRepository { + + @Query("select t from NikshayTU t where t.nikshayDistrictID = :districtID and t.deleted = false order by t.tUName") + List findByDistrictID(@Param("districtID") Integer districtID); + + @Query("select t from NikshayTU t where t.nikshayTUID in (:tuIDs) and t.deleted = false") + List findByIds(@Param("tuIDs") List tuIDs); +} diff --git a/src/main/java/com/iemr/admin/repo/nikshay/NikshayVillageFacilityMappingRepo.java b/src/main/java/com/iemr/admin/repo/nikshay/NikshayVillageFacilityMappingRepo.java new file mode 100644 index 0000000..55ba9b5 --- /dev/null +++ b/src/main/java/com/iemr/admin/repo/nikshay/NikshayVillageFacilityMappingRepo.java @@ -0,0 +1,42 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.admin.repo.nikshay; + +import java.util.List; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import com.iemr.admin.data.nikshay.NikshayVillageFacilityMapping; + +@Repository +public interface NikshayVillageFacilityMappingRepo extends CrudRepository { + + @Query("select m from NikshayVillageFacilityMapping m where m.nikshayFacilityID in (:facilityIDs)") + List findByFacilityIDs(@Param("facilityIDs") List facilityIDs); + + // Reverse lookup: given a beneficiary's existing village ID, find their Nikshay Facility/TU + @Query("select m from NikshayVillageFacilityMapping m where m.amritVillageID = :amritVillageID") + List findByAmritVillageID(@Param("amritVillageID") Integer amritVillageID); +} diff --git a/src/main/java/com/iemr/admin/repo/nikshay/NikshayVillageRepo.java b/src/main/java/com/iemr/admin/repo/nikshay/NikshayVillageRepo.java new file mode 100644 index 0000000..6f0741b --- /dev/null +++ b/src/main/java/com/iemr/admin/repo/nikshay/NikshayVillageRepo.java @@ -0,0 +1,38 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.admin.repo.nikshay; + +import java.util.List; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import com.iemr.admin.data.nikshay.NikshayVillage; + +@Repository +public interface NikshayVillageRepo extends CrudRepository { + + @Query("select v from NikshayVillage v where v.nikshayFacilityID in (:facilityIDs) and v.deleted = false order by v.villageName") + List findByFacilityIDs(@Param("facilityIDs") List facilityIDs); +} diff --git a/src/main/java/com/iemr/admin/repository/rolemaster/M_UserservicerolemappingForRoleProviderAdminRepo.java b/src/main/java/com/iemr/admin/repository/rolemaster/M_UserservicerolemappingForRoleProviderAdminRepo.java index 759be90..b6937b5 100644 --- a/src/main/java/com/iemr/admin/repository/rolemaster/M_UserservicerolemappingForRoleProviderAdminRepo.java +++ b/src/main/java/com/iemr/admin/repository/rolemaster/M_UserservicerolemappingForRoleProviderAdminRepo.java @@ -49,15 +49,14 @@ public interface M_UserservicerolemappingForRoleProviderAdminRepo * serviceProviderID, Integer serviceID); */ - @Query("SELECT sm.serviceName as serviceName," + + @Query("SELECT DISTINCT sm.serviceName as serviceName," + " ssm.serviceID," + " sm.isNational as isNational," + " ssm.statusID" + " FROM M_UserservicerolemappingForRoleProviderAdmin usr" + " JOIN usr.stateServiceMapping ssm" + " JOIN ssm.serviceMaster sm" + - " WHERE usr.userID = :userID AND ssm.statusID = 2 AND usr.deleted = false" + - " GROUP BY sm.serviceName") + " WHERE usr.userID = :userID AND ssm.statusID = 2 AND usr.deleted = false") ArrayList getServiceByServiceProviderIds(@Param("userID") Integer serviceProviderID); @Query("SELECT distinct ssm.stateID," + " sm.stateName, " + " ssm.providerServiceMapID," + " ssm.statusID" diff --git a/src/main/java/com/iemr/admin/service/bulkRegistration/BulkRegistrationServiceImpl.java b/src/main/java/com/iemr/admin/service/bulkRegistration/BulkRegistrationServiceImpl.java index d1b5a9f..4bd0012 100644 --- a/src/main/java/com/iemr/admin/service/bulkRegistration/BulkRegistrationServiceImpl.java +++ b/src/main/java/com/iemr/admin/service/bulkRegistration/BulkRegistrationServiceImpl.java @@ -305,7 +305,7 @@ private void saveUserUser(Employee employee, Integer row, String authorization, mUser.setTitleID(getTitleId(employee.getTitle())); mUser.setFirstName(employee.getFirstName()); mUser.setLastName(employee.getLastName()); - mUser.setUserName(employee.getContactNo()); + mUser.setUserName(employee.getUserName()); mUser.setdOB(convertStringIntoDate(employee.getDob())); mUser.setEmployeeID(employee.getUserName()); mUser.setEmergencyContactNo(String.valueOf(employee.getEmergencyContactNo())); diff --git a/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterInter.java b/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterInter.java index 81b5f45..d6feae0 100644 --- a/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterInter.java +++ b/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterInter.java @@ -166,6 +166,9 @@ Boolean checkingEmpDetails(String userName, String aadhaarNo, String getpAN, Str // Fix 2: cascade soft-delete asha_supervisor_mapping rows when a user is deactivated void cascadeDeleteAshaMappingsForUser(Integer userID); + // Soft-delete other active mappings for same user+service, excluding the row being updated + int softDeleteOldMappings(Integer userID, Integer providerServiceMapID, Integer excludeUSRMappingID); + // Fix 2: smart cascade — for supervisor with multiple facilities, only delete mappings for this facility void cascadeDeleteAshaMappingsForDeactivation(M_UserServiceRoleMapping2 usrRole); diff --git a/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterServiceImpl.java b/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterServiceImpl.java index f4c07f3..f80e4d2 100644 --- a/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterServiceImpl.java +++ b/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterServiceImpl.java @@ -1024,6 +1024,11 @@ public void cascadeDeleteAshaMappingsForUser(Integer userID) { ashaSupervisorMappingService.cascadeDeleteByUserID(userID, "Admin"); } + @Override + public int softDeleteOldMappings(Integer userID, Integer providerServiceMapID, Integer excludeUSRMappingID) { + return employeeMasterRepo.softDeleteOldMappings(userID, providerServiceMapID, excludeUSRMappingID); + } + @Override public void cascadeDeleteAshaMappingsForDeactivation(M_UserServiceRoleMapping2 usrRole) { Integer userID = usrRole.getUserID(); @@ -1218,8 +1223,17 @@ public ArrayList getMappedRole(Integer serviceProvider Object[] row = stateDistrictMap.get(mapping.getuSRMappingID()); if (mapping.getStateID() == null && row[1] != null) mapping.setStateID((Integer) row[1]); if (mapping.getStateName() == null && row[2] != null) mapping.setStateName((String) row[2]); - if (mapping.getWorkingDistrictID() == null && row[3] != null) mapping.setWorkingDistrictID(String.valueOf(row[3])); - if (mapping.getWorkingDistrictName() == null && row[4] != null) mapping.setWorkingDistrictName((String) row[4]); + // Stop TB's DistrictID column holds a Nikshay district ID, not an + // AMRIT one - getDirectStateDistrictByMappingIDs joins it against + // AMRIT's m_district, which for Stop TB resolves to whatever AMRIT + // district happens to share that same numeric ID (a coincidence, + // not real data - e.g. Nikshay's Angul (292) resolving to AMRIT's + // Uttar Kannad, ID 292). Skip the district patch for Stop TB only. + boolean isStopTB = "Stop TB".equals(mapping.getServiceName()); + if (!isStopTB) { + if (mapping.getWorkingDistrictID() == null && row[3] != null) mapping.setWorkingDistrictID(String.valueOf(row[3])); + if (mapping.getWorkingDistrictName() == null && row[4] != null) mapping.setWorkingDistrictName((String) row[4]); + } if (mapping.getBlockID() == null && row[5] != null) mapping.setBlockID((Integer) row[5]); if (mapping.getBlockName() == null && row[6] != null) mapping.setBlockName((String) row[6]); } diff --git a/src/main/java/com/iemr/admin/service/employeemaster/EmployeeSignatureServiceImpl.java b/src/main/java/com/iemr/admin/service/employeemaster/EmployeeSignatureServiceImpl.java index b22175c..58e5cce 100644 --- a/src/main/java/com/iemr/admin/service/employeemaster/EmployeeSignatureServiceImpl.java +++ b/src/main/java/com/iemr/admin/service/employeemaster/EmployeeSignatureServiceImpl.java @@ -27,6 +27,7 @@ import com.iemr.admin.data.employeemaster.EmployeeSignature; import com.iemr.admin.repo.employeemaster.EmployeeSignatureRepo; +import org.json.JSONObject; @Service public class EmployeeSignatureServiceImpl implements EmployeeSignatureService { diff --git a/src/main/java/com/iemr/admin/to/employeemaster/Previleges1097_3.java b/src/main/java/com/iemr/admin/to/employeemaster/Previleges1097_3.java index d8bb0f3..4c6a757 100644 --- a/src/main/java/com/iemr/admin/to/employeemaster/Previleges1097_3.java +++ b/src/main/java/com/iemr/admin/to/employeemaster/Previleges1097_3.java @@ -34,6 +34,10 @@ public class Previleges1097_3 { private String[] villageID; private String[] villageName; private Integer facilityID; + // Comma-joined list of Nikshay TU/Facility IDs (e.g. "12,45,78"), matching + // M_UserServiceRoleMapping2's NikshayTUID/NikshayFacilityID columns. + private String nikshayTUID; + private String nikshayFacilityID; public Integer getProviderServiceMapID() { return providerServiceMapID; @@ -95,4 +99,16 @@ public Integer getFacilityID() { public void setFacilityID(Integer facilityID) { this.facilityID = facilityID; } + public String getNikshayTUID() { + return nikshayTUID; + } + public void setNikshayTUID(String nikshayTUID) { + this.nikshayTUID = nikshayTUID; + } + public String getNikshayFacilityID() { + return nikshayFacilityID; + } + public void setNikshayFacilityID(String nikshayFacilityID) { + this.nikshayFacilityID = nikshayFacilityID; + } } diff --git a/src/main/java/com/iemr/admin/utils/JwtUserIdValidationFilter.java b/src/main/java/com/iemr/admin/utils/JwtUserIdValidationFilter.java index 2eca160..db771d1 100644 --- a/src/main/java/com/iemr/admin/utils/JwtUserIdValidationFilter.java +++ b/src/main/java/com/iemr/admin/utils/JwtUserIdValidationFilter.java @@ -111,7 +111,6 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo } - try { String jwtFromCookie = getJwtTokenFromCookies(request); String jwtFromHeader = request.getHeader(Constants.JWT_TOKEN); From f78178cf964f00f2c42b9f232708126c74ae41f3 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Mon, 3 Aug 2026 19:45:53 +0530 Subject: [PATCH 04/10] fix: Stop TB bulk user upload district validation and qualification crash Stop TB bulk user uploads were failing 100% of the time: District validation ran against AMRIT's stale m_District table (missing post-2022 AP districts), and an unmatched Qualification silently resolved to ID 0, crashing the insert on FK_User_UserQualification instead of failing cleanly. The insert gate also only checked for missing fields, not invalid ones, so rows with invalid data still reached the DB. - Route District resolution through Nikshay's own district master (m_nikshay_district) for Stop TB service-line uploads only, via a new serviceProviderID -> ServiceID=12 check; every other service line keeps using m_District unchanged. - Add the missing "Qualification is invalid" validation check. - Fix the insert gate to check validationErrors.isEmpty() instead of a redundant chain that ignored "invalid" (non-empty but unmatched) values. --- .../M_ProviderServiceMappingRepo.java | 3 + .../BulkRegistrationServiceImpl.java | 71 +++++++++++++++++-- 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/iemr/admin/repository/provideronboard/M_ProviderServiceMappingRepo.java b/src/main/java/com/iemr/admin/repository/provideronboard/M_ProviderServiceMappingRepo.java index 7cec8d2..3ba9b89 100644 --- a/src/main/java/com/iemr/admin/repository/provideronboard/M_ProviderServiceMappingRepo.java +++ b/src/main/java/com/iemr/admin/repository/provideronboard/M_ProviderServiceMappingRepo.java @@ -36,4 +36,7 @@ public interface M_ProviderServiceMappingRepo extends CrudRepository 0 FROM M_ProviderServiceMapping u WHERE u.serviceProviderID = :serviceProviderID AND u.serviceID = :serviceID AND deleted=false") + boolean existsByServiceProviderIDAndServiceID(@Param("serviceProviderID") Integer serviceProviderID, @Param("serviceID") Integer serviceID); + } diff --git a/src/main/java/com/iemr/admin/service/bulkRegistration/BulkRegistrationServiceImpl.java b/src/main/java/com/iemr/admin/service/bulkRegistration/BulkRegistrationServiceImpl.java index 4bd0012..18568de 100644 --- a/src/main/java/com/iemr/admin/service/bulkRegistration/BulkRegistrationServiceImpl.java +++ b/src/main/java/com/iemr/admin/service/bulkRegistration/BulkRegistrationServiceImpl.java @@ -5,8 +5,13 @@ import com.iemr.admin.data.bulkuser.EmployeeList; import com.iemr.admin.data.employeemaster.*; import com.iemr.admin.data.locationmaster.M_District; +import com.iemr.admin.data.nikshay.NikshayDistrict; +import com.iemr.admin.data.nikshay.NikshayState; import com.iemr.admin.data.rolemaster.StateMasterForRole; import com.iemr.admin.repo.employeemaster.V_ShowuserRepo; +import com.iemr.admin.repo.nikshay.NikshayDistrictRepo; +import com.iemr.admin.repo.nikshay.NikshayStateRepo; +import com.iemr.admin.repository.provideronboard.M_ProviderServiceMappingRepo; import com.iemr.admin.service.employeemaster.EmployeeMasterInter; import com.iemr.admin.service.locationmaster.LocationMasterServiceInter; import com.iemr.admin.service.rolemaster.Role_MasterInter; @@ -60,6 +65,20 @@ public class BulkRegistrationServiceImpl implements BulkRegistrationService { @Autowired EmployeeXmlService employeeXmlService; + @Autowired + private NikshayStateRepo nikshayStateRepo; + @Autowired + private NikshayDistrictRepo nikshayDistrictRepo; + @Autowired + private M_ProviderServiceMappingRepo providerServiceMappingRepo; + + // ServiceID for "Stop TB" in m_servicemaster. Stop TB users' District is + // resolved against Nikshay's own district master (m_nikshay_district) instead + // of AMRIT's m_District, because AMRIT's district data is stale for several + // states post the 2022 district reorganizations while Nikshay's is current. + // Every other service line is unaffected and keeps using m_District as before. + private static final Integer STOP_TB_SERVICE_ID = 12; + public ArrayList errorLogs = new ArrayList<>(); public ArrayList m_bulkUser = new ArrayList<>(); public ArrayList m_UserDemographics = new ArrayList<>(); @@ -101,6 +120,7 @@ public static String escapeXmlSpecialChars(String xml) { private void saveUserUser(Employee employee, Integer row, String authorization, String createdBy, Integer serviceProviderID) throws Exception { + boolean isStopTB = providerServiceMappingRepo.existsByServiceProviderIDAndServiceID(serviceProviderID, STOP_TB_SERVICE_ID); List validationErrors = new ArrayList<>(); BulkRegistrationError bulkRegistrationErrors_ = new BulkRegistrationError(); M_User1 mUser = new M_User1(); @@ -232,6 +252,12 @@ private void saveUserUser(Employee employee, Integer row, String authorization, validationErrors.add("Qualification is missing"); } + if (!employee.getQualification().isEmpty()) { + if (getQualificationId(employee.getQualification()) == 0) { + validationErrors.add("Qualification is invalid."); + + } + } if (employee.getState().isEmpty()) { validationErrors.add("Current State is missing."); @@ -246,7 +272,9 @@ private void saveUserUser(Employee employee, Integer row, String authorization, validationErrors.add("Current District is missing."); } if (!employee.getDistrict().isEmpty()) { - if (getDistrictId(employee.getDistrict()) == 0) { + int districtId = isStopTB ? getNikshayDistrictId(employee.getState(), employee.getDistrict()) + : getDistrictId(employee.getDistrict()); + if (districtId == 0) { validationErrors.add("Current District is invalid."); } @@ -266,7 +294,9 @@ private void saveUserUser(Employee employee, Integer row, String authorization, } if (!employee.getPermanentDistrict().isEmpty()) { - if (getDistrictId(employee.getPermanentDistrict()) == 0) { + int permDistrictId = isStopTB ? getNikshayDistrictId(employee.getPermanentState(), employee.getPermanentDistrict()) + : getDistrictId(employee.getPermanentDistrict()); + if (permDistrictId == 0) { validationErrors.add("Permanent District is invalid."); } @@ -299,7 +329,7 @@ private void saveUserUser(Employee employee, Integer row, String authorization, // showLogger(employee); - if (!employee.getTitle().isEmpty() && !employee.getFirstName().isEmpty() && !employee.getLastName().isEmpty() && !employee.getContactNo().isEmpty() && !employee.getEmergencyContactNo().isEmpty() && !employee.getDob().isEmpty() && !employee.getUserName().isEmpty() && !employee.getPassword().isEmpty() && !employee.getState().isEmpty() && !employee.getDistrict().isEmpty() && !employee.getPermanentState().isEmpty() && !employee.getPermanentDistrict().isEmpty() && !employee.getGender().isEmpty() && !employee.getQualification().isEmpty() && isValidDate(convertStringIntoDate(employee.getDob()).toString()) && isValidDate(convertStringIntoDate(employee.getDateOfJoining()).toString())) { + if (validationErrors.isEmpty()) { try { mUser.setTitleID(getTitleId(employee.getTitle())); @@ -372,7 +402,9 @@ private void saveUserUser(Employee employee, Integer row, String authorization, } if (!employee.getPermanentDistrict().isEmpty()) { - mUserDemographics.setPermDistrictID(getDistrictId(employee.getPermanentDistrict())); + mUserDemographics.setPermDistrictID(isStopTB + ? getNikshayDistrictId(employee.getPermanentState(), employee.getPermanentDistrict()) + : getDistrictId(employee.getPermanentDistrict())); } mUserDemographics.setIsPermanent(false); @@ -399,7 +431,9 @@ private void saveUserUser(Employee employee, Integer row, String authorization, } mUserDemographics.setIsPresent(false); if (!employee.getDistrict().isEmpty()) { - mUserDemographics.setDistrictID(getDistrictId(employee.getDistrict())); + mUserDemographics.setDistrictID(isStopTB + ? getNikshayDistrictId(employee.getState(), employee.getDistrict()) + : getDistrictId(employee.getDistrict())); } if (!employee.getPincode().isEmpty()) { @@ -645,6 +679,33 @@ public int getDistrictId(String districtName) { } + // Stop TB-only equivalent of getDistrictId(): resolves against Nikshay's own + // district master (m_nikshay_district) instead of AMRIT's m_District, since + // Nikshay's data reflects current district boundaries (e.g. post-2022 AP + // reorganization) while m_District does not. Not used for any other service + // line. + public int getNikshayDistrictId(String stateName, String districtName) { + if (stateName == null || stateName.isEmpty() || districtName == null || districtName.isEmpty()) { + return 0; + } + + int nikshayStateId = nikshayStateRepo.findAllActive().stream() + .filter(s -> s.getStateName().equalsIgnoreCase(stateName)) + .map(NikshayState::getNikshayStateID) + .findFirst() + .orElse(0); + + if (nikshayStateId == 0) { + return 0; + } + + return nikshayDistrictRepo.findByStateID(nikshayStateId).stream() + .filter(d -> d.getDistrictName().equalsIgnoreCase(districtName)) + .map(NikshayDistrict::getNikshayDistrictID) + .findFirst() + .orElse(0); + } + public ArrayList getAllState() { return roleMasterInter.getAllState(); From 28ffe68bad5d624436e9e4178b29fa2145356386 Mon Sep 17 00:00:00 2001 From: Saurav Mishra <80103738+SauravBizbRolly@users.noreply.github.com> Date: Wed, 5 Aug 2026 12:16:55 +0530 Subject: [PATCH 05/10] fix update user issue and role issue (#143) --- .../employeemaster/EmployeeMasterRepoo.java | 4 + .../employeemaster/M_DesignationRepo.java | 2 + .../BulkRegistrationServiceImpl.java | 326 +++++++++++++++++- .../employeemaster/EmployeeMasterInter.java | 6 + .../EmployeeMasterServiceImpl.java | 35 ++ 5 files changed, 371 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/iemr/admin/repo/employeemaster/EmployeeMasterRepoo.java b/src/main/java/com/iemr/admin/repo/employeemaster/EmployeeMasterRepoo.java index e2b017a..17819cc 100644 --- a/src/main/java/com/iemr/admin/repo/employeemaster/EmployeeMasterRepoo.java +++ b/src/main/java/com/iemr/admin/repo/employeemaster/EmployeeMasterRepoo.java @@ -90,4 +90,8 @@ M_User1 checkingEmpDetails(@Param("userName") String userName, @Param("aadhaarNo M_User1 findByUserID(Integer userID); List findByUserIDIn(List userIDs); + + M_User1 findEmployeeByContactForUpdate(String contactNo, Integer userID); + + M_User1 findEmployeeAadhaarNoForUpdate(String aadhaarNo, Integer userID); } diff --git a/src/main/java/com/iemr/admin/repo/employeemaster/M_DesignationRepo.java b/src/main/java/com/iemr/admin/repo/employeemaster/M_DesignationRepo.java index e141dd8..6c78708 100644 --- a/src/main/java/com/iemr/admin/repo/employeemaster/M_DesignationRepo.java +++ b/src/main/java/com/iemr/admin/repo/employeemaster/M_DesignationRepo.java @@ -22,6 +22,7 @@ package com.iemr.admin.repo.employeemaster; import java.util.ArrayList; +import java.util.List; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; @@ -35,4 +36,5 @@ public interface M_DesignationRepo extends CrudRepository getDesinationlist(); + List findByDesignationName(String designationString); } diff --git a/src/main/java/com/iemr/admin/service/bulkRegistration/BulkRegistrationServiceImpl.java b/src/main/java/com/iemr/admin/service/bulkRegistration/BulkRegistrationServiceImpl.java index 18568de..162a470 100644 --- a/src/main/java/com/iemr/admin/service/bulkRegistration/BulkRegistrationServiceImpl.java +++ b/src/main/java/com/iemr/admin/service/bulkRegistration/BulkRegistrationServiceImpl.java @@ -8,6 +8,8 @@ import com.iemr.admin.data.nikshay.NikshayDistrict; import com.iemr.admin.data.nikshay.NikshayState; import com.iemr.admin.data.rolemaster.StateMasterForRole; +import com.iemr.admin.repo.employeemaster.M_DesignationRepo; +import com.iemr.admin.repo.employeemaster.M_UserDemographicsRepo; import com.iemr.admin.repo.employeemaster.V_ShowuserRepo; import com.iemr.admin.repo.nikshay.NikshayDistrictRepo; import com.iemr.admin.repo.nikshay.NikshayStateRepo; @@ -51,9 +53,16 @@ public class BulkRegistrationServiceImpl implements BulkRegistrationService { public static final String FILE_PATH = "error_log.xlsx"; // Excel file path public List bulkRegistrationErrors = new ArrayList<>(); + @Autowired + private M_UserDemographicsRepo m_UserDemographicsRepo; + @Autowired private EmployeeMasterInter employeeMasterInter; + + @Autowired + private M_DesignationRepo m_DesignationRepo; + @Autowired private Role_MasterInter roleMasterInter; @@ -95,7 +104,7 @@ public void registerBulkUser(String xml, String authorization,String userName,In logger.info("employee_list" + employeeList.getEmployees().toString()); totalEmployeeListSize = employeeList.getEmployees().size(); for (int i = 0; i < employeeList.getEmployees().size(); i++) { - saveUserUser(employeeList.getEmployees().get(i), i, authorization,userName,serviceProviderID); + processUserUser(employeeList.getEmployees().get(i), i, authorization,userName,serviceProviderID); } @@ -117,7 +126,31 @@ public static String escapeXmlSpecialChars(String xml) { // Only escape & that are not already part of valid XML entities return xml.replaceAll("&(?!amp;|lt;|gt;|apos;|quot;|#\\d+;)", "&"); } + /** + * Common entry point — decides create vs update based on whether the user already exists, + * then delegates to the respective method. + */ + private void processUserUser(Employee employee, Integer row, String authorization, String createdBy, Integer serviceProviderID) throws Exception { + if (employee.getUserName() == null || employee.getUserName().isEmpty()) { + List validationErrors = new ArrayList<>(); + BulkRegistrationError bulkRegistrationErrors_ = new BulkRegistrationError(); + validationErrors.add("Please Enter UserName"); + logAndCollectError(row, employee, validationErrors, bulkRegistrationErrors_); + return; + } + + String checkUserIsExist = employeeMasterInter.FindEmployeeName(employee.getUserName()); + logger.info("checkUserIsExist" + checkUserIsExist); + + if (checkUserIsExist.equalsIgnoreCase("usernotexist")) { + // user not found → create flow + saveUserUser(employee, row, authorization, createdBy, serviceProviderID); + } else { + // user already exists → update flow + updateUserUser(employee, row, authorization, createdBy, serviceProviderID); + } + } private void saveUserUser(Employee employee, Integer row, String authorization, String createdBy, Integer serviceProviderID) throws Exception { boolean isStopTB = providerServiceMappingRepo.existsByServiceProviderIDAndServiceID(serviceProviderID, STOP_TB_SERVICE_ID); @@ -504,6 +537,291 @@ private void saveUserUser(Employee employee, Integer row, String authorization, */ + private void updateUserUser(Employee employee, Integer row, String authorization, String modifiedBy, Integer serviceProviderID) throws Exception { + boolean isStopTB = providerServiceMappingRepo.existsByServiceProviderIDAndServiceID(serviceProviderID, STOP_TB_SERVICE_ID); + + List validationErrors = new ArrayList<>(); + BulkRegistrationError bulkRegistrationErrors_ = new BulkRegistrationError(); + + logger.info("employee_list update flow" + employee.toString()); + + // fetch existing user record to update + M_User1 existingUser = employeeMasterInter.FindEmployeeName1(employee.getUserName()); + if (existingUser == null) { + validationErrors.add("User not found for update"); + logAndCollectError(row, employee, validationErrors, bulkRegistrationErrors_); + return; + } + Integer userID = existingUser.getUserID(); + + // duplicate contact check excluding current user + String checkContactIsExist = employeeMasterInter.FindEmployeeContactForUpdate(employee.getContactNo(), userID); + + if (checkContactIsExist.equalsIgnoreCase("contactnotexist")) { + + if (employee.getTitle() == null || employee.getTitle().isEmpty()) { + validationErrors.add("Title is missing."); + } + if (!employee.getTitle().isEmpty() && getTitleId(employee.getTitle()) == 0) { + validationErrors.add("Title is invalid."); + } + + if (employee.getFirstName() == null || employee.getFirstName().isEmpty()) { + validationErrors.add("First Name is missing."); + } + if (!employee.getFirstName().isEmpty()) { + if (employee.getFirstName().length() > 50) { + validationErrors.add("First name is invalid."); + } + if (isNumeric(employee.getFirstName())) { + validationErrors.add("First name is invalid."); + } + } + + if (!employee.getMiddleName().isEmpty()) { + if (employee.getMiddleName().length() > 50) { + validationErrors.add("Middle name is invalid."); + } + if (isNumeric(employee.getMiddleName())) { + validationErrors.add("Middle name is invalid."); + } + } + + if (employee.getLastName() == null || employee.getLastName().isEmpty()) { + validationErrors.add("Last Name is missing."); + } + if (!employee.getLastName().isEmpty()) { + if (employee.getLastName().length() > 50) { + validationErrors.add("Last name is invalid."); + } + if (isNumeric(employee.getLastName())) { + validationErrors.add("Last name is invalid."); + } + } + + if (employee.getGender().isEmpty()) { + validationErrors.add("Gender is missing"); + } + if (employee.getContactNo().isEmpty()) { + validationErrors.add("Contact number missing"); + } + if (!employee.getContactNo().isEmpty() && !isValidPhoneNumber(String.valueOf(employee.getContactNo()))) { + validationErrors.add("Contact Number is invalid"); + } + + if (employee.getDesignation().isEmpty()) { + validationErrors.add("Designation is missing"); + } + + if (employee.getEmergencyContactNo().isEmpty()) { + validationErrors.add("Emergency contact number is missing"); + } + if (!employee.getEmergencyContactNo().isEmpty() && !isValidPhoneNumber(String.valueOf(employee.getEmergencyContactNo()))) { + validationErrors.add("Emergency Contact Number is invalid."); + } + + if (employee.getDob().isEmpty()) { + validationErrors.add("Date of Birth is missing."); + } + if (!employee.getDob().isEmpty() && !isValidDate(convertStringIntoDate(employee.getDob()).toString())) { + validationErrors.add("Date of Birth is invalid."); + } + + if (employee.getEmail().isEmpty()) { + validationErrors.add("Email is missing."); + } + if (!employee.getEmail().isEmpty() && !employee.getEmail().matches(EMAIL_REGEX)) { + validationErrors.add("Invalid Email format."); + } + + // password optional on update — only validated if provided + if (!employee.getPassword().isEmpty() && employee.getPassword().length() < 6) { + validationErrors.add("Please enter a valid password."); + } + + if (!employee.getAadhaarNo().isEmpty()) { + String checkAadhaarIsExist = employeeMasterInter.FindEmployeeAadhaarForUpdate(employee.getAadhaarNo(), userID); + if (!checkAadhaarIsExist.equalsIgnoreCase("aadhaarnotexist")) { + validationErrors.add("Duplicate aadhaar number found"); + } + if (isValidAadhar(employee.getAadhaarNo())) { + validationErrors.add("Aadhaar number is invalid"); + } + } + + if (employee.getQualification().isEmpty()) { + validationErrors.add("Qualification is missing"); + } + + if (employee.getState().isEmpty()) { + validationErrors.add("Current State is missing."); + } + if (!employee.getState().isEmpty() && getStateId(employee.getState()) == 0) { + validationErrors.add("Current State is invalid."); + } + + if (employee.getDistrict().isEmpty()) { + validationErrors.add("Current District is missing."); + } + if (!employee.getDistrict().isEmpty() && getDistrictId(employee.getDistrict()) == 0) { + validationErrors.add("Current District is invalid."); + } + + if (employee.getPermanentState().isEmpty()) { + validationErrors.add("Permanent State is missing."); + } + if (!employee.getPermanentState().isEmpty() && getStateId(employee.getPermanentState()) == 0) { + validationErrors.add("Permanent State is invalid."); + } + + if (employee.getPermanentDistrict().isEmpty()) { + validationErrors.add("Permanent District is missing."); + } + if (!employee.getPermanentDistrict().isEmpty() && getDistrictId(employee.getPermanentDistrict()) == 0) { + validationErrors.add("Permanent District is invalid."); + } + + if (employee.getDateOfJoining().isEmpty()) { + validationErrors.add("Date of Joining is missing."); + } + if (!employee.getDateOfJoining().isEmpty() && !isValidDate(convertStringIntoDate(employee.getDateOfJoining()).toString())) { + validationErrors.add("Date of Joining is invalid."); + } + + if (!validationErrors.isEmpty()) { + logAndCollectError(row, employee, validationErrors, bulkRegistrationErrors_); + } + + boolean coreFieldsValid = !employee.getTitle().isEmpty() && !employee.getFirstName().isEmpty() + && !employee.getLastName().isEmpty() && !employee.getContactNo().isEmpty() + && !employee.getEmergencyContactNo().isEmpty() && !employee.getDob().isEmpty() + && !employee.getUserName().isEmpty() && !employee.getState().isEmpty() + && !employee.getDistrict().isEmpty() && !employee.getPermanentState().isEmpty() + && !employee.getPermanentDistrict().isEmpty() && !employee.getGender().isEmpty() + && !employee.getQualification().isEmpty() + && isValidDate(convertStringIntoDate(employee.getDob()).toString()) + && isValidDate(convertStringIntoDate(employee.getDateOfJoining()).toString()); + + if (coreFieldsValid) { + try { + M_User1 mUser = existingUser; + mUser.setUserID(mUser.getUserID()); + mUser.setTitleID(getTitleId(employee.getTitle())); + mUser.setFirstName(employee.getFirstName()); + mUser.setLastName(employee.getLastName()); + mUser.setdOB(convertStringIntoDate(employee.getDob())); + mUser.setEmergencyContactNo(String.valueOf(employee.getEmergencyContactNo())); + mUser.setContactNo(String.valueOf(employee.getContactNo())); + + if (!employee.getMiddleName().isEmpty()) { + mUser.setMiddleName(employee.getMiddleName()); + } + if (!employee.getDesignation().isEmpty()) { + mUser.setDesignationID(getDesignationId(employee.getDesignation())); + mUser.setDesignationName(employee.getDesignation()); + } + if (!employee.getAadhaarNo().isEmpty() && isValidAadhar(employee.getAadhaarNo())) { + mUser.setAadhaarNo(String.valueOf(employee.getAadhaarNo())); + } + if (!employee.getPan().isEmpty()) { + mUser.setpAN(employee.getPan()); + } + mUser.setEmailID(employee.getEmail()); + mUser.setGenderID(Short.parseShort(String.valueOf(getGenderId(employee.getGender())))); + if (!employee.getQualification().isEmpty()) { + mUser.setQualificationID(getQualificationId(employee.getQualification())); + } + mUser.setdOJ(convertStringIntoDate(employee.getDateOfJoining())); + mUser.setModifiedBy(modifiedBy); + mUser.setServiceProviderID(serviceProviderID); + + // password touched only if a new one was supplied + if (!employee.getPassword().isEmpty()) { + mUser.setPassword(generateStrongPassword(employee.getPassword())); + } + + M_User1 updatedUser = employeeMasterInter.saveBulkUserEmployee(mUser); + + M_UserDemographics mUserDemographics = employeeMasterInter.getUserDemographicsByUserID(updatedUser.getUserID()); + if (mUserDemographics == null) { + mUserDemographics = new M_UserDemographics(); + mUserDemographics.setUserID(updatedUser.getUserID()); + mUserDemographics.setCountryID(91); + } + + if (!employee.getCommunity().isEmpty()) { + mUserDemographics.setCommunityID(getCommunityId(employee.getCommunity())); + } + if (!employee.getReligion().isEmpty()) { + mUserDemographics.setReligionID(getReligionStringId(employee.getReligion())); + } + mUserDemographics.setModifiedBy(modifiedBy); + + if (!employee.getPermanentAddressLine1().isEmpty()) { + mUserDemographics.setPermAddressLine1(employee.getPermanentAddressLine1()); + } + if (!employee.getPermanentState().isEmpty()) { + mUserDemographics.setPermStateID(getStateId(employee.getPermanentState())); + } + if (!employee.getPermanentDistrict().isEmpty()) { + mUserDemographics.setPermDistrictID(getDistrictId(employee.getPermanentDistrict())); + } + if (!employee.getPermanentPincode().isEmpty()) { + mUserDemographics.setPermPinCode(Integer.valueOf(employee.getPermanentPincode())); + } + if (!employee.getMotherName().isEmpty()) { + mUserDemographics.setMothersName(employee.getMotherName()); + } + if (!employee.getFatherName().isEmpty()) { + mUserDemographics.setFathersName(employee.getFatherName()); + } + if (!employee.getAddressLine1().isEmpty()) { + mUserDemographics.setAddressLine1(employee.getAddressLine1()); + } + if (!employee.getState().isEmpty()) { + mUserDemographics.setStateID(getStateId(employee.getState())); + } + if (!employee.getDistrict().isEmpty()) { + int districtId = isStopTB ? getNikshayDistrictId(employee.getState(), employee.getDistrict()) + : getDistrictId(employee.getDistrict()); + if (districtId == 0) { + validationErrors.add("Current District is invalid."); + + } + } + if (!employee.getPincode().isEmpty()) { + mUserDemographics.setPinCode(employee.getPincode().toString()); + } + + m_UserDemographicsRepo.save(mUserDemographics); + + m_bulkUser.add(mUser); + m_UserDemographics.add(mUserDemographics); + + } catch (Exception e) { + errorLogs.add("Row :" + (row + 1) + e.getMessage()); + logAndCollectError(row, employee, validationErrors, bulkRegistrationErrors_); + } + } + + } else { + validationErrors.add("Contact No already belongs to another user"); + logAndCollectError(row, employee, validationErrors, bulkRegistrationErrors_); + } + } + + // shared helper — same duplicated block ko replace kar diya + private void logAndCollectError(Integer row, Employee employee, List validationErrors, + BulkRegistrationError bulkRegistrationErrors_) { + if (!validationErrors.isEmpty()) { + errorLogs.add("Row " + (row + 1) + ": " + String.join(", ", validationErrors)); + bulkRegistrationErrors_.setRowNumber((row + 1)); + bulkRegistrationErrors_.setUserName(employee.getUserName()); + bulkRegistrationErrors_.setError(validationErrors); + bulkRegistrationErrors.add(bulkRegistrationErrors_); + } + } + private boolean isValidDate(String dateStr) { try { String[] parts = dateStr.split("-"); @@ -621,8 +939,12 @@ private int getTitleId(String titleString) { public int getDesignationId(String designationString) { + if(!m_DesignationRepo.findByDesignationName(designationString).isEmpty()){ + return m_DesignationRepo.findByDesignationName(designationString).get(0).getDesignationID(); - return 20; + }else { + return 0; + } } diff --git a/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterInter.java b/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterInter.java index d6feae0..859f087 100644 --- a/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterInter.java +++ b/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterInter.java @@ -201,4 +201,10 @@ Boolean checkingEmpDetails(String userName, String aadhaarNo, String getpAN, Str M_UserServiceRoleMapping2 deleteuserrolemapTM(M_UserServiceRoleMapping2 pre) throws Exception; M_User1 saveBulkUserEmployee(M_User1 mUser); + + String FindEmployeeContactForUpdate(String contactNo, Integer userID); + + String FindEmployeeAadhaarForUpdate(String aadhaarNo, Integer userID); + + M_UserDemographics getUserDemographicsByUserID(Integer userID); } diff --git a/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterServiceImpl.java b/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterServiceImpl.java index f80e4d2..f09d0bd 100644 --- a/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterServiceImpl.java +++ b/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterServiceImpl.java @@ -1508,4 +1508,39 @@ public M_User1 saveBulkUserEmployee(M_User1 mUser) { return data; } + @Override + public String FindEmployeeContactForUpdate(String contactNo, Integer userID) { + logger.info("EmployeeMasterServiceImpl.FindEmployeeContactForUpdate - start"); + M_User1 data = employeeMasterRepo11.findEmployeeByContactForUpdate(contactNo, userID); + String result; + if (data == null) { + result = "contactnotexist"; + } else { + result = "contactexist"; + } + logger.info("EmployeeMasterServiceImpl.FindEmployeeContactForUpdate - finish"); + return result; + } + + @Override + public String FindEmployeeAadhaarForUpdate(String aadhaarNo, Integer userID) { + logger.info("EmployeeMasterServiceImpl.FindEmployeeAadhaarForUpdate - start"); + M_User1 data = employeeMasterRepo11.findEmployeeAadhaarNoForUpdate(aadhaarNo, userID); + String result; + if (data == null) { + result = "aadhaarnotexist"; + } else { + result = "aadhaarexist"; + } + logger.info("EmployeeMasterServiceImpl.FindEmployeeAadhaarForUpdate - finish"); + return result; + } + + @Override + public M_UserDemographics getUserDemographicsByUserID(Integer userID) { + logger.info("EmployeeMasterServiceImpl.getUserDemographicsByUserID - start"); + M_UserDemographics data = m_UserDemographicsRepo.findByUserID(userID); + logger.info("EmployeeMasterServiceImpl.getUserDemographicsByUserID - finish"); + return data; + } } From c9e1d851798790ec0a8fecc0ecdf073fa571cfda Mon Sep 17 00:00:00 2001 From: Saurav Mishra <80103738+SauravBizbRolly@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:52:52 +0530 Subject: [PATCH 06/10] Fix/admin bulk registration role issue (#144) * fix update user issue and role issue * fix update user issue and role issue --- .../iemr/admin/repo/employeemaster/EmployeeMasterRepoo.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/iemr/admin/repo/employeemaster/EmployeeMasterRepoo.java b/src/main/java/com/iemr/admin/repo/employeemaster/EmployeeMasterRepoo.java index 17819cc..74fe406 100644 --- a/src/main/java/com/iemr/admin/repo/employeemaster/EmployeeMasterRepoo.java +++ b/src/main/java/com/iemr/admin/repo/employeemaster/EmployeeMasterRepoo.java @@ -91,7 +91,9 @@ M_User1 checkingEmpDetails(@Param("userName") String userName, @Param("aadhaarNo List findByUserIDIn(List userIDs); - M_User1 findEmployeeByContactForUpdate(String contactNo, Integer userID); + @Query("SELECT u FROM M_User1 u WHERE u.contactNo=:contactNo AND u.userID<>:userID AND deleted=false") + M_User1 findEmployeeByContactForUpdate(@Param("contactNo") String contactNo, @Param("userID") Integer userID); - M_User1 findEmployeeAadhaarNoForUpdate(String aadhaarNo, Integer userID); + @Query("SELECT u FROM M_User1 u WHERE u.aadhaarNo=:aadhaar AND u.userID<>:userID AND deleted=false") + M_User1 findEmployeeAadhaarNoForUpdate(@Param("aadhaar") String aadhaar, @Param("userID") Integer userID); } From c6ada0ae1333edbc3d1ec5fda2eb9c431f21ff5f Mon Sep 17 00:00:00 2001 From: SnehaRH <77656297+snehar-nd@users.noreply.github.com> Date: Thu, 3 Sep 2026 10:37:33 +0530 Subject: [PATCH 07/10] USER ID / Login ID Update for Mobile Number Change (#146) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(employee-master): validate username uniqueness on edit, stop clearing Employee ID Edit no longer silently nulls Employee ID on every save — the edit form doesn't submit that field, and the code was unconditionally copying the (missing) value over the existing one. Also checks the new username isn't already taken by another employee before saving, rather than allowing silent duplicates. * feat(admin): add username rename API with audit trail propagation CreatedBy/ModifiedBy across AMRIT store the username as a denormalised string rather than a UserID foreign key, so renaming a user has to be propagated by hand. Adds POST /username/renameUsername plus a /preview variant that reports the same per-table row counts without committing. The sweep covers m_user and 28 RMNCH/FLW tables across db_iemr and db_identity, in one transaction so a partial rename cannot be left behind. Column naming was verified against the AMRIT-DB migrations: db_identity is uniformly CreatedBy/ModifiedBy while db_iemr is split between created_by/updated_by and CreatedBy/ModifiedBy. Each table takes two UPDATEs, one per audit column, rather than a single combined statement. Setting both columns off a CreatedBy match would overwrite ModifiedBy on rows a different user last touched, destroying their attribution, and would miss rows this user only modified. Usernames are bound as parameters; only whitelist constants are interpolated. m_user is updated first so a unique-key rejection on UserName or EmployeeID happens before anything else is written, and the new name is capped at 12 characters when contact numbers are included because m_user.ContactNo is varchar(12). Scope is deliberately these tables only — it is not every table carrying an audit column, so the old username survives elsewhere. Co-Authored-By: Claude Opus 5 * fix(admin): make username rename preview index-friendly and exact Preview timed out behind nginx on UAT (504). Each of the 28 tables was counted with `createdBy = :u OR modifiedBy = :u`, and an OR across two columns cannot use a single-column index on either — so every table was a full scan. Split into two single-column counts, matching the two UPDATEs the rename already issues, so both can use the new indexes in AMRIT-DB V96/V19. Splitting also makes the numbers honest: a row the user both created and last modified is updated by each statement, so counting it once under-reported what rename() goes on to report. Preview now also counts m_user, which it previously skipped while the rename reported it, so the two line up row for row. Corrects the ELIGIBLE_COUPLE_TRACKING entry to eligible_couple_tracking — the schema creates it lowercase, and table names are case-sensitive on Linux MySQL. Co-Authored-By: Claude Opus 5 * Revert "fix(admin): make username rename preview index-friendly and exact" This reverts commit cd20896e681521ac3e5582208fdcf21659d3516a. * refactor(admin): drive username rename updates by primary key Reworks the sweep to the supplied query shape: each UPDATE now selects the rows to touch by primary key through a derived table rather than filtering on the audit column directly. The SELECT ... AS temp wrapper is what MySQL requires to read from the same table an UPDATE targets (error 1093), and driving by PK keeps the statements usable under safe-update mode. Carries each table's primary key in the registry; all 28 were verified against the AMRIT-DB migrations. Both audit columns are now set off a CreatedBy match, per the supplied queries, replacing the earlier split into one statement per column. Preview counts CreatedBy alone to match, and still reports m_user, so preview and result line up row for row. Keeps eligible_couple_tracking lower case: the schema creates it that way and MySQL table names are case sensitive on Linux. Co-Authored-By: Claude Opus 5 * feat(admin): drop the username rename preview endpoint Removes POST /username/renameUsername/preview along with the service method behind it, the two counting queries it used, and the now-unused preview flag on the response. /username/renameUsername is unchanged and remains the only endpoint. Co-Authored-By: Claude Opus 5 * feat(admin): accept an optional new Employee ID on username rename EmployeeID was hardcoded to the new username in both branches of the m_user update. It is now only touched when the request supplies a new value, so renaming a username no longer implies changing the Employee ID. The SET clause is built from what was actually asked for. Splits the uniqueness check in two. UserName and EmployeeID hold separate UNIQUE keys on m_user, so each conflicts only with its own column — the previous combined check rejected a new username merely because some row used it as an Employee ID, which was only correct while the two were forced to match. Both checks exclude the row being renamed, so re-entering the value that row already holds is not reported as a conflict with itself. Co-Authored-By: Claude Opus 5 * feat(admin): add username/employee ID availability endpoint Adds POST /username/checkAvailability so the screen can report a duplicate as it is typed rather than at submit. It reuses the same repository checks rename() runs, so the two cannot disagree — including counting soft-deleted rows, which still hold m_user's UNIQUE keys. Co-Authored-By: Claude Opus 5 * feat(admin): update username and employee ID independently Both new values are now optional. Each is normalised to null when blank or already equal to what the row holds, and the m_user UPDATE is built from whichever columns actually change — so a username-only change, an employee-ID-only change, or both are all valid. The request is rejected only when neither would change anything. The 28-table audit sweep is skipped entirely when the username is unchanged: CreatedBy/ModifiedBy record the username, so an employee-ID-only change leaves every audit row already correct. That also keeps such a change off the unindexed scan path. Drops the updateEmployeeId flag (supplying a value is the intent) and the /username/checkAvailability endpoint, since the screen now uses the same m/FindEmployeeByName and m/FindEmployeeDetails checks as Employee Master. Co-Authored-By: Claude Opus 5 * feat(admin): address the rename target by userID Takes userID on the rename request and uses it as the identity of the row being changed. The username is the very thing being renamed, so a string match is a poor way to address it; the primary key is not. The m_user UPDATE is now keyed on UserID, and both uniqueness checks exclude self by UserID rather than by username — the same way the Employee Master edit path does it via findEmployeeByNameForUpdate. The old username is now read from the row rather than trusted from the request, and a mismatch is rejected: if the row was renamed after the screen loaded its list, sweeping on the stale username would repoint the wrong audit records. Co-Authored-By: Claude Opus 5 * fix(admin): return the employee ID in the rename response The response carried only the username pair, so a caller had no way to see what happened to the employee ID. Adds oldEmployeeId (read before the update) and newEmployeeId. Also adds userNameUpdated/employeeIdUpdated booleans. OutputResponse re-serialises the envelope with a GsonBuilder that has no serializeNulls, so any null field drops out of the JSON entirely — which is why an unset employee ID appears to be missing rather than null. A primitive boolean always survives that pass, so the caller can tell "unchanged" from "absent". Co-Authored-By: Claude Opus 5 * feat(admin): drop row/table counts from the rename response Removes rowsPerTable, tablesAffected and totalRowsAffected. The response now reports only what changed: the username and employee ID before and after, plus the flags saying whether each was written. The total row count is still summed and logged, so the scale of a rename remains traceable without exposing the table list to callers. Co-Authored-By: Claude Opus 5 * Revert "fix(employee-master): validate username uniqueness on edit, stop clearing Employee ID" This reverts commit 4f6371f061910a5cc951e105dd0ff34f3a57b278. * revert: undo the employee edit page username/Employee ID changes Reverts 4f6371f — the editUserDetails uniqueness check, the Employee ID null-guard and findEmployeeByNameForUpdate. Verified independent of the Change Username feature: nothing in the username package called any of it, so the rename API is unaffected. Drops the comment that pointed at the removed query. Note this restores the prior editUserDetails behaviour, including the unconditional setEmployeeID that nulls Employee ID when the edit form does not submit that field. Co-Authored-By: Claude Opus 5 * style(admin): remove comments from the username rename classes Pure deletions — no code changed. GPL license headers kept. Co-Authored-By: Claude Opus 5 * fix(admin): address the scan findings on the username rename Log injection (UsernameRenameServiceImpl): the rename start line logged the old/new username and employee ID straight from the request, so a value containing CR/LF could forge log entries. Those values are now flattened to a safe character set and length-capped before logging, and the line also carries the userID, which is an integer and cannot be tampered with. Dynamic SQL (UsernameRenameRepository): schema, table and column names cannot be bound as parameters, so they are interpolated. Each is now checked against a strict SQL-identifier pattern before it can reach a statement, so anything that is not a plain identifier throws instead of being concatenated in. All caller-supplied data was, and remains, bound. Verified every identifier in the table registry satisfies the pattern. Co-Authored-By: Claude Opus 5 * fix(admin): stop logging request-derived strings on rename The sanitising helper did not clear Sonar's taint analysis — a custom regex replace is not recognised as a sanitiser, so the rule still traced request data into the logger and the Quality Gate stayed at Security Rating C. Logs the userID and two booleans saying which columns are changing instead. Nothing request-derived reaches the log, so the taint path is gone rather than filtered. The values themselves are still available in the response and in m_user. Co-Authored-By: Claude Opus 5 * fix(admin): build rename SQL from constants, not formatted strings Sonar reported three MAJOR vulnerabilities for dynamically formatted SQL in UsernameRenameRepository, holding New Code at Security Rating C and failing the Quality Gate. Each query call now receives a constant rather than a formatted string: - the two uniqueness counts become two fixed queries instead of one with an interpolated column name - the m_user update selects one of five constants covering the possible combinations, instead of joining a SET clause at runtime - the per-table rename SQL is built once in AuditTable's constructor and read back through a getter, so the query call takes a plain value Identifier validation moves to AuditTable, where the identifiers now live, and rejects anything that is not a plain SQL identifier. The generated SQL is unchanged: verified byte-identical for the per-table rename and for all eight combinations of the m_user update. Co-Authored-By: Claude Opus 5 --------- Co-authored-by: Sneha Co-authored-by: Claude Opus 5 --- .../username/UsernameRenameController.java | 64 +++++++ .../model/username/UsernameRenameRequest.java | 73 ++++++++ .../username/UsernameRenameResponse.java | 81 +++++++++ .../username/UsernameAuditTables.java | 120 +++++++++++++ .../username/UsernameRenameRepository.java | 143 ++++++++++++++++ .../username/UsernameRenameService.java | 29 ++++ .../username/UsernameRenameServiceImpl.java | 158 ++++++++++++++++++ 7 files changed, 668 insertions(+) create mode 100644 src/main/java/com/iemr/admin/controller/username/UsernameRenameController.java create mode 100644 src/main/java/com/iemr/admin/model/username/UsernameRenameRequest.java create mode 100644 src/main/java/com/iemr/admin/model/username/UsernameRenameResponse.java create mode 100644 src/main/java/com/iemr/admin/repository/username/UsernameAuditTables.java create mode 100644 src/main/java/com/iemr/admin/repository/username/UsernameRenameRepository.java create mode 100644 src/main/java/com/iemr/admin/service/username/UsernameRenameService.java create mode 100644 src/main/java/com/iemr/admin/service/username/UsernameRenameServiceImpl.java diff --git a/src/main/java/com/iemr/admin/controller/username/UsernameRenameController.java b/src/main/java/com/iemr/admin/controller/username/UsernameRenameController.java new file mode 100644 index 0000000..907311f --- /dev/null +++ b/src/main/java/com/iemr/admin/controller/username/UsernameRenameController.java @@ -0,0 +1,64 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.admin.controller.username; + +import javax.ws.rs.core.MediaType; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import com.iemr.admin.model.username.UsernameRenameRequest; +import com.iemr.admin.service.username.UsernameRenameService; +import com.iemr.admin.utils.mapper.OutputMapper; +import com.iemr.admin.utils.response.OutputResponse; + +import io.swagger.v3.oas.annotations.Operation; +import jakarta.servlet.http.HttpServletRequest; + +@RestController +@RequestMapping(value = "/username") +public class UsernameRenameController { + private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); + + @Autowired + private UsernameRenameService usernameRenameService; + + @Operation(summary = "Rename a username and repoint its audit records") + @RequestMapping(value = "/renameUsername", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON, headers = "Authorization") + public String renameUsername(@RequestBody UsernameRenameRequest renameRequest, HttpServletRequest request) { + OutputResponse response = new OutputResponse(); + try { + logger.info("renameUsername received request"); + response.setResponse(OutputMapper.gsonWithoutExpose().toJson(usernameRenameService.rename(renameRequest))); + } catch (Exception e) { + logger.error("renameUsername failed", e); + response.setError(e); + } + logger.info("renameUsername sending response"); + return response.toString(); + } +} diff --git a/src/main/java/com/iemr/admin/model/username/UsernameRenameRequest.java b/src/main/java/com/iemr/admin/model/username/UsernameRenameRequest.java new file mode 100644 index 0000000..b0f4998 --- /dev/null +++ b/src/main/java/com/iemr/admin/model/username/UsernameRenameRequest.java @@ -0,0 +1,73 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.admin.model.username; + +public class UsernameRenameRequest { + private Integer userID; + + private String oldUserName; + private String newUserName; + + private String newEmployeeId; + + private boolean updateContactFields = true; + + public Integer getUserID() { + return userID; + } + + public void setUserID(Integer userID) { + this.userID = userID; + } + + public String getOldUserName() { + return oldUserName; + } + + public void setOldUserName(String oldUserName) { + this.oldUserName = oldUserName; + } + + public String getNewUserName() { + return newUserName; + } + + public void setNewUserName(String newUserName) { + this.newUserName = newUserName; + } + + public String getNewEmployeeId() { + return newEmployeeId; + } + + public void setNewEmployeeId(String newEmployeeId) { + this.newEmployeeId = newEmployeeId; + } + + public boolean isUpdateContactFields() { + return updateContactFields; + } + + public void setUpdateContactFields(boolean updateContactFields) { + this.updateContactFields = updateContactFields; + } +} diff --git a/src/main/java/com/iemr/admin/model/username/UsernameRenameResponse.java b/src/main/java/com/iemr/admin/model/username/UsernameRenameResponse.java new file mode 100644 index 0000000..993f749 --- /dev/null +++ b/src/main/java/com/iemr/admin/model/username/UsernameRenameResponse.java @@ -0,0 +1,81 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.admin.model.username; + +public class UsernameRenameResponse { + private String oldUserName; + private String newUserName; + + private String oldEmployeeId; + private String newEmployeeId; + + private boolean userNameUpdated; + private boolean employeeIdUpdated; + + public boolean isUserNameUpdated() { + return userNameUpdated; + } + + public void setUserNameUpdated(boolean userNameUpdated) { + this.userNameUpdated = userNameUpdated; + } + + public boolean isEmployeeIdUpdated() { + return employeeIdUpdated; + } + + public void setEmployeeIdUpdated(boolean employeeIdUpdated) { + this.employeeIdUpdated = employeeIdUpdated; + } + + public String getOldEmployeeId() { + return oldEmployeeId; + } + + public void setOldEmployeeId(String oldEmployeeId) { + this.oldEmployeeId = oldEmployeeId; + } + + public String getNewEmployeeId() { + return newEmployeeId; + } + + public void setNewEmployeeId(String newEmployeeId) { + this.newEmployeeId = newEmployeeId; + } + + public String getOldUserName() { + return oldUserName; + } + + public void setOldUserName(String oldUserName) { + this.oldUserName = oldUserName; + } + + public String getNewUserName() { + return newUserName; + } + + public void setNewUserName(String newUserName) { + this.newUserName = newUserName; + } +} diff --git a/src/main/java/com/iemr/admin/repository/username/UsernameAuditTables.java b/src/main/java/com/iemr/admin/repository/username/UsernameAuditTables.java new file mode 100644 index 0000000..7afffac --- /dev/null +++ b/src/main/java/com/iemr/admin/repository/username/UsernameAuditTables.java @@ -0,0 +1,120 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.admin.repository.username; + +import java.util.List; +import java.util.regex.Pattern; + +public final class UsernameAuditTables { + private UsernameAuditTables() { + } + + private static final Pattern SQL_IDENTIFIER = Pattern.compile("\\w+(\\.\\w+)?"); + + private static String identifier(String name) { + if (name == null || !SQL_IDENTIFIER.matcher(name).matches()) { + throw new IllegalArgumentException("Illegal SQL identifier: " + name); + } + return name; + } + + public static final class AuditTable { + private final String qualifiedName; + private final String createdByColumn; + private final String modifiedByColumn; + private final String primaryKeyColumn; + private final String renameSql; + + public AuditTable(String qualifiedName, String createdByColumn, String modifiedByColumn, + String primaryKeyColumn) { + this.qualifiedName = identifier(qualifiedName); + this.createdByColumn = identifier(createdByColumn); + this.modifiedByColumn = identifier(modifiedByColumn); + this.primaryKeyColumn = identifier(primaryKeyColumn); + this.renameSql = String.format( + "UPDATE %1$s SET %2$s = :newUserName, %3$s = :newUserName " + + "WHERE %4$s IN (SELECT %4$s FROM (SELECT %4$s FROM %1$s WHERE %2$s = :oldUserName)" + + " AS temp)", + this.qualifiedName, this.createdByColumn, this.modifiedByColumn, this.primaryKeyColumn); + } + + public String getRenameSql() { + return renameSql; + } + + public String getQualifiedName() { + return qualifiedName; + } + + public String getCreatedByColumn() { + return createdByColumn; + } + + public String getModifiedByColumn() { + return modifiedByColumn; + } + + public String getPrimaryKeyColumn() { + return primaryKeyColumn; + } + } + + private static AuditTable pascal(String qualifiedName, String primaryKeyColumn) { + return new AuditTable(qualifiedName, "CreatedBy", "ModifiedBy", primaryKeyColumn); + } + + private static AuditTable snake(String qualifiedName, String primaryKeyColumn) { + return new AuditTable(qualifiedName, "created_by", "updated_by", primaryKeyColumn); + } + + public static final List TABLES = List.of( + pascal("db_identity.i_beneficiarydetails_rmnch", "beneficiaryDetails_RmnchId"), + pascal("db_identity.i_beneficiaryfamilymapping", "BenFamilyMapId"), + pascal("db_identity.i_beneficiarydetails", "BeneficiaryDetailsId"), + pascal("db_identity.i_beneficiarymapping", "BenMapId"), + pascal("db_identity.i_beneficiaryidentity", "BenIdentityId"), + pascal("db_identity.i_householddetails", "houseHoldDetailsId"), + pascal("db_identity.i_beneficiaryimage", "BenImageId"), + pascal("db_identity.i_beneficiaryaddress", "BenAddressID"), + pascal("db_identity.i_beneficiaryservicemapping", "BenServiceMapID"), + pascal("db_identity.m_beneficiaryregidmapping", "BenRegId"), + pascal("db_identity.i_bornbirthdeatils", "BornBirthDeatilsId"), + pascal("db_identity.i_beneficiarycontacts", "BenContactsID"), + pascal("db_identity.i_beneficiaryconsent", "BenConsentID"), + pascal("db_identity.i_benfamilytag", "BenFamilyTagId"), + + snake("db_iemr.eligible_couple_tracking", "id"), + snake("db_iemr.t_pregnant_woman_register", "id"), + snake("db_iemr.t_eligible_couple_register", "id"), + snake("db_iemr.t_delivery_outcome", "id"), + snake("db_iemr.t_infant_register", "id"), + snake("db_iemr.t_pnc_visit", "ID"), + snake("db_iemr.t_anc_visit", "ID"), + snake("db_iemr.t_child_register", "ID"), + snake("db_iemr.t_pmsma", "id"), + + pascal("db_iemr.t_cbacdetails", "id"), + pascal("db_iemr.t_pnccare", "id"), + pascal("db_iemr.t_anccare", "ID"), + pascal("db_iemr.t_benvisitdetail", "BenVisitID"), + pascal("db_iemr.t_childvaccinedetail1", "ID")); +} diff --git a/src/main/java/com/iemr/admin/repository/username/UsernameRenameRepository.java b/src/main/java/com/iemr/admin/repository/username/UsernameRenameRepository.java new file mode 100644 index 0000000..e0a3088 --- /dev/null +++ b/src/main/java/com/iemr/admin/repository/username/UsernameRenameRepository.java @@ -0,0 +1,143 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.admin.repository.username; + +import java.math.BigInteger; +import java.util.List; + +import org.springframework.stereotype.Repository; + +import com.iemr.admin.repository.username.UsernameAuditTables.AuditTable; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.Query; + +@Repository +public class UsernameRenameRepository { + /** + * Schema, table and column names cannot be bound as parameters, so they are + * interpolated. Every identifier is checked against this before it reaches a + * statement, so a value that is not a plain SQL identifier can never be + * concatenated in. All caller-supplied data is bound, never interpolated. + */ + private static final String COUNT_BY_USER_NAME = + "SELECT COUNT(*) FROM db_iemr.m_user WHERE UserName = :value AND UserID <> :excludeUserID"; + + private static final String COUNT_BY_EMPLOYEE_ID = + "SELECT COUNT(*) FROM db_iemr.m_user WHERE EmployeeID = :value AND UserID <> :excludeUserID"; + + private static final String SET_USER_NAME = + "UPDATE db_iemr.m_user SET UserName = :newUserName WHERE UserID = :userID"; + + private static final String SET_USER_NAME_AND_CONTACTS = + "UPDATE db_iemr.m_user SET UserName = :newUserName, EmergencyContactNo = :newUserName," + + " ContactNo = :newUserName WHERE UserID = :userID"; + + private static final String SET_EMPLOYEE_ID = + "UPDATE db_iemr.m_user SET EmployeeID = :newEmployeeId WHERE UserID = :userID"; + + private static final String SET_USER_NAME_AND_EMPLOYEE_ID = + "UPDATE db_iemr.m_user SET UserName = :newUserName, EmployeeID = :newEmployeeId WHERE UserID = :userID"; + + private static final String SET_USER_NAME_CONTACTS_AND_EMPLOYEE_ID = + "UPDATE db_iemr.m_user SET UserName = :newUserName, EmergencyContactNo = :newUserName," + + " ContactNo = :newUserName, EmployeeID = :newEmployeeId WHERE UserID = :userID"; + + @PersistenceContext + private EntityManager entityManager; + + public long renameInTable(AuditTable table, String oldUserName, String newUserName) { + Query query = entityManager.createNativeQuery(table.getRenameSql()); + query.setParameter("newUserName", newUserName); + query.setParameter("oldUserName", oldUserName); + return query.executeUpdate(); + } + + public String currentUserName(Integer userID) { + return single("SELECT UserName FROM db_iemr.m_user WHERE UserID = :userID", userID); + } + + public String currentEmployeeId(Integer userID) { + return single("SELECT EmployeeID FROM db_iemr.m_user WHERE UserID = :userID", userID); + } + + private String single(String sql, Integer userID) { + Query query = entityManager.createNativeQuery(sql); + query.setParameter("userID", userID); + List rows = query.getResultList(); + return rows.isEmpty() ? null : (String) rows.get(0); + } + + public long renameUserRow(Integer userID, String newUserName, String newEmployeeId, + boolean updateContactFields) { + String sql = selectUserUpdate(newUserName, newEmployeeId, updateContactFields); + if (sql == null) { + return 0; + } + + Query query = entityManager.createNativeQuery(sql); + query.setParameter("userID", userID); + if (newUserName != null) { + query.setParameter("newUserName", newUserName); + } + if (newEmployeeId != null) { + query.setParameter("newEmployeeId", newEmployeeId); + } + return query.executeUpdate(); + } + + private static String selectUserUpdate(String newUserName, String newEmployeeId, boolean updateContactFields) { + if (newUserName == null) { + return newEmployeeId == null ? null : SET_EMPLOYEE_ID; + } + if (newEmployeeId == null) { + return updateContactFields ? SET_USER_NAME_AND_CONTACTS : SET_USER_NAME; + } + return updateContactFields ? SET_USER_NAME_CONTACTS_AND_EMPLOYEE_ID : SET_USER_NAME_AND_EMPLOYEE_ID; + } + + public boolean userNameTaken(String userName, Integer excludeUserID) { + return countMatching(COUNT_BY_USER_NAME, userName, excludeUserID) > 0; + } + + public boolean employeeIdTaken(String employeeId, Integer excludeUserID) { + return countMatching(COUNT_BY_EMPLOYEE_ID, employeeId, excludeUserID) > 0; + } + + private long countMatching(String sql, String value, Integer excludeUserID) { + Query query = entityManager.createNativeQuery(sql); + query.setParameter("value", value); + query.setParameter("excludeUserID", excludeUserID); + return toLong(query.getSingleResult()); + } + + private long toLong(Object result) { + if (result == null) { + return 0L; + } + if (result instanceof BigInteger bigInteger) { + return bigInteger.longValue(); + } + return ((Number) result).longValue(); + } +} diff --git a/src/main/java/com/iemr/admin/service/username/UsernameRenameService.java b/src/main/java/com/iemr/admin/service/username/UsernameRenameService.java new file mode 100644 index 0000000..84d8b78 --- /dev/null +++ b/src/main/java/com/iemr/admin/service/username/UsernameRenameService.java @@ -0,0 +1,29 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.admin.service.username; + +import com.iemr.admin.model.username.UsernameRenameRequest; +import com.iemr.admin.model.username.UsernameRenameResponse; + +public interface UsernameRenameService { + UsernameRenameResponse rename(UsernameRenameRequest request) throws Exception; +} diff --git a/src/main/java/com/iemr/admin/service/username/UsernameRenameServiceImpl.java b/src/main/java/com/iemr/admin/service/username/UsernameRenameServiceImpl.java new file mode 100644 index 0000000..e22c905 --- /dev/null +++ b/src/main/java/com/iemr/admin/service/username/UsernameRenameServiceImpl.java @@ -0,0 +1,158 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.admin.service.username; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.iemr.admin.model.username.UsernameRenameRequest; +import com.iemr.admin.model.username.UsernameRenameResponse; +import com.iemr.admin.repository.username.UsernameAuditTables; +import com.iemr.admin.repository.username.UsernameAuditTables.AuditTable; +import com.iemr.admin.repository.username.UsernameRenameRepository; + +@Service +public class UsernameRenameServiceImpl implements UsernameRenameService { + private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); + + private static final int MAX_USERNAME_LENGTH = 20; + private static final int MAX_EMPLOYEE_ID_LENGTH = 20; + + private static final int MAX_CONTACT_LENGTH = 12; + + @Autowired + private UsernameRenameRepository usernameRenameRepository; + + @Override + @Transactional(rollbackFor = Exception.class) + public UsernameRenameResponse rename(UsernameRenameRequest request) throws Exception { + validate(request); + + String oldUserName = request.getOldUserName(); + String newUserName = request.getNewUserName(); + String newEmployeeId = request.getNewEmployeeId(); + logger.info("Username rename starting: userID {}, userNameChanging {}, employeeIdChanging {}", + request.getUserID(), newUserName != null, newEmployeeId != null); + + UsernameRenameResponse response = newResponse(request); + response.setOldEmployeeId(usernameRenameRepository.currentEmployeeId(request.getUserID())); + + long rowsUpdated = usernameRenameRepository.renameUserRow(request.getUserID(), newUserName, newEmployeeId, + request.isUpdateContactFields()); + + if (newUserName != null) { + for (AuditTable table : UsernameAuditTables.TABLES) { + rowsUpdated += usernameRenameRepository.renameInTable(table, oldUserName, newUserName); + } + } + + logger.info("Username rename complete: {} rows updated", rowsUpdated); + return response; + } + + private UsernameRenameResponse newResponse(UsernameRenameRequest request) { + UsernameRenameResponse response = new UsernameRenameResponse(); + response.setOldUserName(request.getOldUserName()); + response.setNewUserName(request.getNewUserName()); + response.setNewEmployeeId(request.getNewEmployeeId()); + response.setUserNameUpdated(request.getNewUserName() != null); + response.setEmployeeIdUpdated(request.getNewEmployeeId() != null); + return response; + } + + private void validate(UsernameRenameRequest request) throws Exception { + if (request == null) { + throw new IllegalArgumentException("Request body is required"); + } + + if (request.getUserID() == null) { + throw new IllegalArgumentException("User ID is required"); + } + + String storedUserName = usernameRenameRepository.currentUserName(request.getUserID()); + if (storedUserName == null) { + throw new IllegalArgumentException("No user found with ID " + request.getUserID()); + } + + String oldUserName = trimToNull(request.getOldUserName()); + if (oldUserName != null && !oldUserName.equals(storedUserName)) { + throw new IllegalArgumentException("User " + request.getUserID() + " is now named " + storedUserName + + ", not " + oldUserName + ". Reload the user list and try again."); + } + request.setOldUserName(storedUserName); + oldUserName = storedUserName; + + request.setNewUserName(resolveNewUserName(request, oldUserName)); + request.setNewEmployeeId(resolveNewEmployeeId(request)); + + if (request.getNewUserName() == null && request.getNewEmployeeId() == null) { + throw new IllegalArgumentException("Nothing to update — enter a new username or a new employee ID"); + } + } + + private String resolveNewUserName(UsernameRenameRequest request, String oldUserName) throws Exception { + String newUserName = trimToNull(request.getNewUserName()); + if (newUserName == null || newUserName.equals(oldUserName)) { + return null; + } + if (newUserName.length() > MAX_USERNAME_LENGTH) { + throw new IllegalArgumentException( + "New username exceeds " + MAX_USERNAME_LENGTH + " characters (m_user.UserName limit)"); + } + if (request.isUpdateContactFields() && newUserName.length() > MAX_CONTACT_LENGTH) { + throw new IllegalArgumentException("New username exceeds " + MAX_CONTACT_LENGTH + + " characters and cannot be written to ContactNo. Either shorten it or " + + "turn off updating contact numbers."); + } + if (usernameRenameRepository.userNameTaken(newUserName, request.getUserID())) { + throw new IllegalArgumentException("Username " + newUserName + " is already in use"); + } + return newUserName; + } + + private String resolveNewEmployeeId(UsernameRenameRequest request) throws Exception { + String newEmployeeId = trimToNull(request.getNewEmployeeId()); + if (newEmployeeId == null + || newEmployeeId.equals(usernameRenameRepository.currentEmployeeId(request.getUserID()))) { + return null; + } + if (newEmployeeId.length() > MAX_EMPLOYEE_ID_LENGTH) { + throw new IllegalArgumentException( + "New employee ID exceeds " + MAX_EMPLOYEE_ID_LENGTH + " characters (m_user.EmployeeID limit)"); + } + if (usernameRenameRepository.employeeIdTaken(newEmployeeId, request.getUserID())) { + throw new IllegalArgumentException("Employee ID " + newEmployeeId + " is already in use"); + } + return newEmployeeId; + } + + private String trimToNull(String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; + } +} From de57d4ff300718468bcd8e150614d737f77fb97e Mon Sep 17 00:00:00 2001 From: Vishwanath Balkur <118195001+vishwab1@users.noreply.github.com> Date: Fri, 11 Sep 2026 13:50:19 +0530 Subject: [PATCH 08/10] Role Mapping Not Saving When Updating Multiple Roles (Registrar/Nurse/Counsellor) (#148) * fix: batched role creates dropping roles; role update wiping sibling roles Two related bugs in the user-role-work-location mapping endpoints, both confirmed via live DB timestamp inspection against a real StopTB user with multiple roles (Registration Officer/Nurse/Counsellor). 1. UserRoleMappings() (POST userRoleMappings, create): the loop built one M_UserServiceRoleMapping2 object across all iterations of the per-role loop and only added it to the save list once, after the loop closed - so a batched request carrying multiple roles (e.g. Nurse + Counsellor together) silently discarded every role but the last one. Object creation and resList1.add() now happen once per role, inside the loop. 2. softDeleteOldMappings (called from updateUserRoleMapping): scoped only by userID + providerServiceMapID, so saving/updating ANY one role's mapping soft-deleted every OTHER active role that user held under the same service line, even roles never mentioned in that request and saved in a completely separate, earlier call. Added roleID to the WHERE clause (and threaded it through EmployeeMasterInter/EmployeeMasterServiceImpl/the controller call site) so it only cleans up true duplicates of the same role. Verified live: after this fix, a user's Registration Officer + Nurse + Counsellor mappings all stay simultaneously active across separate saves, with Registration Officer + Nurse created together in one batched call (matching CreatedDate) and Counsellor from an earlier save left untouched by later, unrelated saves. Co-Authored-By: Claude Sonnet 5 * chore: bump version to 3.8.4 Co-Authored-By: Claude Sonnet 5 --------- Co-authored-by: Claude Sonnet 5 --- pom.xml | 2 +- .../EmployeeMasterController.java | 48 +++++++++++-------- .../employeemaster/EmployeeMasterRepo.java | 11 +++-- .../employeemaster/EmployeeMasterInter.java | 4 +- .../EmployeeMasterServiceImpl.java | 4 +- 5 files changed, 42 insertions(+), 27 deletions(-) diff --git a/pom.xml b/pom.xml index 062df58..9e6a49b 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.iemr.admin admin-api - 3.8.2 + 3.8.4 war Admin-API Admin Page diff --git a/src/main/java/com/iemr/admin/controller/employeemaster/EmployeeMasterController.java b/src/main/java/com/iemr/admin/controller/employeemaster/EmployeeMasterController.java index 33d980c..cfe1979 100644 --- a/src/main/java/com/iemr/admin/controller/employeemaster/EmployeeMasterController.java +++ b/src/main/java/com/iemr/admin/controller/employeemaster/EmployeeMasterController.java @@ -1791,6 +1791,12 @@ public String UserRoleMappings(@RequestBody String userRoleMapping, HttpServletR Previleges1097_3[] predata1 = pre.get(x).getPrevileges(); for (Previleges1097_3 previl : predata1) { + // Each role in the ID array must become its own saved row. This used to + // build a single M_UserServiceRoleMapping2 across all iterations of this + // loop and only add it to resList1 once, after the loop — so a batched + // request carrying multiple roles (e.g. Nurse + Counsellor) silently + // discarded every role but the last one. Moving the object creation and + // the add() call inside the loop saves one row per role, as intended. Priveleges1097_2[] predata2 = previl.getID(); for (Priveleges1097_2 previl1 : predata2) { resDataMap1 = new M_UserServiceRoleMapping2(); @@ -1800,22 +1806,22 @@ public String UserRoleMappings(@RequestBody String userRoleMapping, HttpServletR if (previl1.getTeleConsultation() != null) { resDataMap1.setTeleConsultation(previl1.getTeleConsultation()); } + resDataMap1.setUserID(employeeMaster.get(x).getUserID()); + resDataMap1.setProviderServiceMapID(previl.getProviderServiceMapID()); + resDataMap1.setWorkingLocationID(previl.getWorkingLocationID()); + resDataMap1.setStateID(previl.getStateID()); + resDataMap1.setDistrictID(previl.getDistrictID()); + resDataMap1.setCreatedBy(employeeMaster.get(x).getCreatedBy()); + resDataMap1.setServiceProviderID(employeeMaster.get(x).getServiceProviderID()); + resDataMap1.setBlockID(previl.getBlockID()); + resDataMap1.setBlockName(previl.getBlockName()); + resDataMap1.setVillageID(previl.getVillageID()); + resDataMap1.setVillageName(previl.getVillageName()); + resDataMap1.setFacilityID(previl.getFacilityID()); + resDataMap1.setNikshayTUID(previl.getNikshayTUID()); + resDataMap1.setNikshayFacilityID(previl.getNikshayFacilityID()); + resList1.add(resDataMap1); } - resDataMap1.setUserID(employeeMaster.get(x).getUserID()); - resDataMap1.setProviderServiceMapID(previl.getProviderServiceMapID()); - resDataMap1.setWorkingLocationID(previl.getWorkingLocationID()); - resDataMap1.setStateID(previl.getStateID()); - resDataMap1.setDistrictID(previl.getDistrictID()); - resDataMap1.setCreatedBy(employeeMaster.get(x).getCreatedBy()); - resDataMap1.setServiceProviderID(employeeMaster.get(x).getServiceProviderID()); - resDataMap1.setBlockID(previl.getBlockID()); - resDataMap1.setBlockName(previl.getBlockName()); - resDataMap1.setVillageID(previl.getVillageID()); - resDataMap1.setVillageName(previl.getVillageName()); - resDataMap1.setFacilityID(previl.getFacilityID()); - resDataMap1.setNikshayTUID(previl.getNikshayTUID()); - resDataMap1.setNikshayFacilityID(previl.getNikshayFacilityID()); - resList1.add(resDataMap1); } x++; @@ -1863,11 +1869,15 @@ public String updateUserRoleMapping(@RequestBody String updateUserRoleMapping, H } } - // Soft-delete other active mappings for same user+service to prevent duplicates - // (e.g. old roleID 122 rows left over when new roleID 128 mapping was created) - if (pre.getUserID() != null && pre.getProviderServiceMapID() != null && pre.getuSRMappingID() != null) { + // Soft-delete other active mappings for same user+service+role to prevent + // duplicates (e.g. a stale leftover row still claiming this same role). + // Scoped by roleID as well as user+service — otherwise this deletes every + // OTHER role the user holds under this service line too, not just true + // duplicates of the role being saved here. + if (pre.getUserID() != null && pre.getProviderServiceMapID() != null && pre.getRoleID() != null + && pre.getuSRMappingID() != null) { employeeMasterInter.softDeleteOldMappings( - pre.getUserID(), pre.getProviderServiceMapID(), pre.getuSRMappingID()); + pre.getUserID(), pre.getProviderServiceMapID(), pre.getRoleID(), pre.getuSRMappingID()); } usrRole.setUserID(pre.getUserID()); diff --git a/src/main/java/com/iemr/admin/repo/employeemaster/EmployeeMasterRepo.java b/src/main/java/com/iemr/admin/repo/employeemaster/EmployeeMasterRepo.java index e2bf74f..33929fe 100644 --- a/src/main/java/com/iemr/admin/repo/employeemaster/EmployeeMasterRepo.java +++ b/src/main/java/com/iemr/admin/repo/employeemaster/EmployeeMasterRepo.java @@ -155,10 +155,15 @@ boolean existsByUserIDAndRoleIDAndProviderServiceMapIDAndFacilityIDAndDeletedFal // Fix 2: count active USR rows for supervisor (check if other facilities remain) long countByUserIDAndRoleIDAndDeletedFalse(Integer userID, Integer roleID); - // Soft-delete old duplicate mappings for same user+service, excluding the current row being updated + // Soft-delete old duplicate mappings for the SAME user+service+role, excluding + // the current row being updated. Previously left roleID out of the WHERE + // clause entirely, so updating any one role's mapping soft-deleted every + // OTHER active role that user held under the same service line (e.g. saving + // Registration Officer wiped out an unrelated, already-active Counsellor + // mapping) instead of only cleaning up true duplicates of the same role. @Transactional @Modifying - @Query("UPDATE M_UserServiceRoleMapping2 u SET u.deleted = true WHERE u.userID = :userID AND u.providerServiceMapID = :providerServiceMapID AND u.uSRMappingID != :excludeUSRMappingID AND u.deleted = false") - int softDeleteOldMappings(@Param("userID") Integer userID, @Param("providerServiceMapID") Integer providerServiceMapID, @Param("excludeUSRMappingID") Integer excludeUSRMappingID); + @Query("UPDATE M_UserServiceRoleMapping2 u SET u.deleted = true WHERE u.userID = :userID AND u.providerServiceMapID = :providerServiceMapID AND u.roleID = :roleID AND u.uSRMappingID != :excludeUSRMappingID AND u.deleted = false") + int softDeleteOldMappings(@Param("userID") Integer userID, @Param("providerServiceMapID") Integer providerServiceMapID, @Param("roleID") Integer roleID, @Param("excludeUSRMappingID") Integer excludeUSRMappingID); } diff --git a/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterInter.java b/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterInter.java index 859f087..9b51b81 100644 --- a/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterInter.java +++ b/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterInter.java @@ -166,8 +166,8 @@ Boolean checkingEmpDetails(String userName, String aadhaarNo, String getpAN, Str // Fix 2: cascade soft-delete asha_supervisor_mapping rows when a user is deactivated void cascadeDeleteAshaMappingsForUser(Integer userID); - // Soft-delete other active mappings for same user+service, excluding the row being updated - int softDeleteOldMappings(Integer userID, Integer providerServiceMapID, Integer excludeUSRMappingID); + // Soft-delete other active mappings for same user+service+role, excluding the row being updated + int softDeleteOldMappings(Integer userID, Integer providerServiceMapID, Integer roleID, Integer excludeUSRMappingID); // Fix 2: smart cascade — for supervisor with multiple facilities, only delete mappings for this facility void cascadeDeleteAshaMappingsForDeactivation(M_UserServiceRoleMapping2 usrRole); diff --git a/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterServiceImpl.java b/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterServiceImpl.java index f09d0bd..16229ca 100644 --- a/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterServiceImpl.java +++ b/src/main/java/com/iemr/admin/service/employeemaster/EmployeeMasterServiceImpl.java @@ -1025,8 +1025,8 @@ public void cascadeDeleteAshaMappingsForUser(Integer userID) { } @Override - public int softDeleteOldMappings(Integer userID, Integer providerServiceMapID, Integer excludeUSRMappingID) { - return employeeMasterRepo.softDeleteOldMappings(userID, providerServiceMapID, excludeUSRMappingID); + public int softDeleteOldMappings(Integer userID, Integer providerServiceMapID, Integer roleID, Integer excludeUSRMappingID) { + return employeeMasterRepo.softDeleteOldMappings(userID, providerServiceMapID, roleID, excludeUSRMappingID); } @Override From 0a93e84a4c9a5648a3b03c5d9ff86c4244e603b5 Mon Sep 17 00:00:00 2001 From: Sneha Date: Mon, 28 Sep 2026 17:06:51 +0530 Subject: [PATCH 09/10] feat: add villageMapping/updateVillage endpoint Replace one village on a user's work location mapping and move the beneficiary addresses that user registered under the old village (i_beneficiaryaddress permanent and current village) to the new one, in a single transaction. The mapping row is locked and checked against the user, the old village must still be mapped, and the new village name is read from m_districtbranchmapping. Co-Authored-By: Claude Opus 5.5 --- .../VillageUpdateController.java | 63 ++++++ .../villagemapping/VillageUpdateRequest.java | 108 +++++++++++ .../villagemapping/VillageUpdateResponse.java | 99 ++++++++++ .../VillageUpdateRepository.java | 118 ++++++++++++ .../villagemapping/VillageUpdateService.java | 29 +++ .../VillageUpdateServiceImpl.java | 180 ++++++++++++++++++ 6 files changed, 597 insertions(+) create mode 100644 src/main/java/com/iemr/admin/controller/villagemapping/VillageUpdateController.java create mode 100644 src/main/java/com/iemr/admin/model/villagemapping/VillageUpdateRequest.java create mode 100644 src/main/java/com/iemr/admin/model/villagemapping/VillageUpdateResponse.java create mode 100644 src/main/java/com/iemr/admin/repository/villagemapping/VillageUpdateRepository.java create mode 100644 src/main/java/com/iemr/admin/service/villagemapping/VillageUpdateService.java create mode 100644 src/main/java/com/iemr/admin/service/villagemapping/VillageUpdateServiceImpl.java diff --git a/src/main/java/com/iemr/admin/controller/villagemapping/VillageUpdateController.java b/src/main/java/com/iemr/admin/controller/villagemapping/VillageUpdateController.java new file mode 100644 index 0000000..ccfac3f --- /dev/null +++ b/src/main/java/com/iemr/admin/controller/villagemapping/VillageUpdateController.java @@ -0,0 +1,63 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.admin.controller.villagemapping; + +import javax.ws.rs.core.MediaType; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import com.iemr.admin.model.villagemapping.VillageUpdateRequest; +import com.iemr.admin.service.villagemapping.VillageUpdateService; +import com.iemr.admin.utils.mapper.OutputMapper; +import com.iemr.admin.utils.response.OutputResponse; + +import io.swagger.v3.oas.annotations.Operation; + +@RestController +@RequestMapping(value = "/villageMapping") +public class VillageUpdateController { + private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); + + @Autowired + private VillageUpdateService villageUpdateService; + + @Operation(summary = "Replace a village on a user's work location mapping and move their beneficiary addresses") + @RequestMapping(value = "/updateVillage", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON, headers = "Authorization") + public String updateVillage(@RequestBody VillageUpdateRequest updateRequest) { + OutputResponse response = new OutputResponse(); + try { + logger.info("updateVillage received request"); + response.setResponse(OutputMapper.gsonWithoutExpose().toJson(villageUpdateService.updateVillage(updateRequest))); + } catch (Exception e) { + logger.error("updateVillage failed", e); + response.setError(e); + } + logger.info("updateVillage sending response"); + return response.toString(); + } +} diff --git a/src/main/java/com/iemr/admin/model/villagemapping/VillageUpdateRequest.java b/src/main/java/com/iemr/admin/model/villagemapping/VillageUpdateRequest.java new file mode 100644 index 0000000..7b6e117 --- /dev/null +++ b/src/main/java/com/iemr/admin/model/villagemapping/VillageUpdateRequest.java @@ -0,0 +1,108 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.admin.model.villagemapping; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class VillageUpdateRequest { + private Integer userID; + private String userName; + + @JsonProperty("uSRMappingID") + private Integer uSRMappingID; + + private Integer oldVillageID; + private String oldVillageName; + + private Integer newVillageID; + private String newVillageName; + + private String modifiedBy; + + public Integer getUserID() { + return userID; + } + + public void setUserID(Integer userID) { + this.userID = userID; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + @JsonProperty("uSRMappingID") + public Integer getuSRMappingID() { + return uSRMappingID; + } + + @JsonProperty("uSRMappingID") + public void setuSRMappingID(Integer uSRMappingID) { + this.uSRMappingID = uSRMappingID; + } + + public Integer getOldVillageID() { + return oldVillageID; + } + + public void setOldVillageID(Integer oldVillageID) { + this.oldVillageID = oldVillageID; + } + + public String getOldVillageName() { + return oldVillageName; + } + + public void setOldVillageName(String oldVillageName) { + this.oldVillageName = oldVillageName; + } + + public Integer getNewVillageID() { + return newVillageID; + } + + public void setNewVillageID(Integer newVillageID) { + this.newVillageID = newVillageID; + } + + public String getNewVillageName() { + return newVillageName; + } + + public void setNewVillageName(String newVillageName) { + this.newVillageName = newVillageName; + } + + public String getModifiedBy() { + return modifiedBy; + } + + public void setModifiedBy(String modifiedBy) { + this.modifiedBy = modifiedBy; + } +} diff --git a/src/main/java/com/iemr/admin/model/villagemapping/VillageUpdateResponse.java b/src/main/java/com/iemr/admin/model/villagemapping/VillageUpdateResponse.java new file mode 100644 index 0000000..95ed8a9 --- /dev/null +++ b/src/main/java/com/iemr/admin/model/villagemapping/VillageUpdateResponse.java @@ -0,0 +1,99 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.admin.model.villagemapping; + +public class VillageUpdateResponse { + private Integer uSRMappingID; + private String userName; + + private Integer oldVillageID; + private String oldVillageName; + private Integer newVillageID; + private String newVillageName; + + private long permanentAddressesUpdated; + private long currentAddressesUpdated; + + public Integer getuSRMappingID() { + return uSRMappingID; + } + + public void setuSRMappingID(Integer uSRMappingID) { + this.uSRMappingID = uSRMappingID; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public Integer getOldVillageID() { + return oldVillageID; + } + + public void setOldVillageID(Integer oldVillageID) { + this.oldVillageID = oldVillageID; + } + + public String getOldVillageName() { + return oldVillageName; + } + + public void setOldVillageName(String oldVillageName) { + this.oldVillageName = oldVillageName; + } + + public Integer getNewVillageID() { + return newVillageID; + } + + public void setNewVillageID(Integer newVillageID) { + this.newVillageID = newVillageID; + } + + public String getNewVillageName() { + return newVillageName; + } + + public void setNewVillageName(String newVillageName) { + this.newVillageName = newVillageName; + } + + public long getPermanentAddressesUpdated() { + return permanentAddressesUpdated; + } + + public void setPermanentAddressesUpdated(long permanentAddressesUpdated) { + this.permanentAddressesUpdated = permanentAddressesUpdated; + } + + public long getCurrentAddressesUpdated() { + return currentAddressesUpdated; + } + + public void setCurrentAddressesUpdated(long currentAddressesUpdated) { + this.currentAddressesUpdated = currentAddressesUpdated; + } +} diff --git a/src/main/java/com/iemr/admin/repository/villagemapping/VillageUpdateRepository.java b/src/main/java/com/iemr/admin/repository/villagemapping/VillageUpdateRepository.java new file mode 100644 index 0000000..6dbbdd4 --- /dev/null +++ b/src/main/java/com/iemr/admin/repository/villagemapping/VillageUpdateRepository.java @@ -0,0 +1,118 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.admin.repository.villagemapping; + +import java.util.List; + +import org.springframework.stereotype.Repository; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.Query; + +@Repository +public class VillageUpdateRepository { + private static final String SELECT_MAPPING_FOR_UPDATE = + "SELECT UserID, Villageid, VillageName, Deleted FROM db_iemr.m_userservicerolemapping" + + " WHERE USRMappingID = :uSRMappingID FOR UPDATE"; + + private static final String SELECT_USER_NAME = + "SELECT UserName FROM db_iemr.m_user WHERE UserID = :userID"; + + private static final String SELECT_VILLAGE_NAME = + "SELECT VillageName FROM db_iemr.m_districtbranchmapping" + + " WHERE DistrictBranchID = :villageID AND (Deleted IS NULL OR Deleted = b'0')"; + + private static final String SET_MAPPING_VILLAGES = + "UPDATE db_iemr.m_userservicerolemapping SET Villageid = :villageIDs, VillageName = :villageNames," + + " ModifiedBy = :modifiedBy WHERE USRMappingID = :uSRMappingID"; + + private static final String SET_PERMANENT_VILLAGE = + "UPDATE db_identity.i_beneficiaryaddress SET PermVillageId = :newVillageID, PermVillage = :newVillageName," + + " ModifiedBy = :modifiedBy WHERE CreatedBy = :userName AND PermVillageId = :oldVillageID"; + + private static final String SET_CURRENT_VILLAGE = + "UPDATE db_identity.i_beneficiaryaddress SET CurrVillageId = :newVillageID, CurrVillage = :newVillageName," + + " ModifiedBy = :modifiedBy WHERE CreatedBy = :userName AND CurrVillageId = :oldVillageID"; + + @PersistenceContext + private EntityManager entityManager; + + /** + * Locks the mapping row for the rest of the transaction and returns + * {UserID, Villageid, VillageName, Deleted}, or null when it does not exist. + */ + public Object[] lockMapping(Integer uSRMappingID) { + Query query = entityManager.createNativeQuery(SELECT_MAPPING_FOR_UPDATE); + query.setParameter("uSRMappingID", uSRMappingID); + List rows = query.getResultList(); + return rows.isEmpty() ? null : (Object[]) rows.get(0); + } + + public String userName(Integer userID) { + return single(SELECT_USER_NAME, "userID", userID); + } + + public String villageName(Integer villageID) { + return single(SELECT_VILLAGE_NAME, "villageID", villageID); + } + + private String single(String sql, String parameter, Integer value) { + Query query = entityManager.createNativeQuery(sql); + query.setParameter(parameter, value); + List rows = query.getResultList(); + return rows.isEmpty() || rows.get(0) == null ? null : rows.get(0).toString(); + } + + public long updateMappingVillages(Integer uSRMappingID, String villageIDs, String villageNames, + String modifiedBy) { + Query query = entityManager.createNativeQuery(SET_MAPPING_VILLAGES); + query.setParameter("villageIDs", villageIDs); + query.setParameter("villageNames", villageNames); + query.setParameter("modifiedBy", modifiedBy); + query.setParameter("uSRMappingID", uSRMappingID); + return query.executeUpdate(); + } + + public long updatePermanentVillage(String userName, Integer oldVillageID, Integer newVillageID, + String newVillageName, String modifiedBy) { + return updateAddressVillage(SET_PERMANENT_VILLAGE, userName, oldVillageID, newVillageID, newVillageName, + modifiedBy); + } + + public long updateCurrentVillage(String userName, Integer oldVillageID, Integer newVillageID, + String newVillageName, String modifiedBy) { + return updateAddressVillage(SET_CURRENT_VILLAGE, userName, oldVillageID, newVillageID, newVillageName, + modifiedBy); + } + + private long updateAddressVillage(String sql, String userName, Integer oldVillageID, Integer newVillageID, + String newVillageName, String modifiedBy) { + Query query = entityManager.createNativeQuery(sql); + query.setParameter("newVillageID", newVillageID); + query.setParameter("newVillageName", newVillageName); + query.setParameter("modifiedBy", modifiedBy); + query.setParameter("userName", userName); + query.setParameter("oldVillageID", oldVillageID); + return query.executeUpdate(); + } +} diff --git a/src/main/java/com/iemr/admin/service/villagemapping/VillageUpdateService.java b/src/main/java/com/iemr/admin/service/villagemapping/VillageUpdateService.java new file mode 100644 index 0000000..6bdde03 --- /dev/null +++ b/src/main/java/com/iemr/admin/service/villagemapping/VillageUpdateService.java @@ -0,0 +1,29 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.admin.service.villagemapping; + +import com.iemr.admin.model.villagemapping.VillageUpdateRequest; +import com.iemr.admin.model.villagemapping.VillageUpdateResponse; + +public interface VillageUpdateService { + VillageUpdateResponse updateVillage(VillageUpdateRequest request) throws Exception; +} diff --git a/src/main/java/com/iemr/admin/service/villagemapping/VillageUpdateServiceImpl.java b/src/main/java/com/iemr/admin/service/villagemapping/VillageUpdateServiceImpl.java new file mode 100644 index 0000000..a73686b --- /dev/null +++ b/src/main/java/com/iemr/admin/service/villagemapping/VillageUpdateServiceImpl.java @@ -0,0 +1,180 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.admin.service.villagemapping; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.iemr.admin.model.villagemapping.VillageUpdateRequest; +import com.iemr.admin.model.villagemapping.VillageUpdateResponse; +import com.iemr.admin.repository.villagemapping.VillageUpdateRepository; + +@Service +public class VillageUpdateServiceImpl implements VillageUpdateService { + private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); + + private static final int MAX_VILLAGE_IDS_LENGTH = 100; + private static final int MAX_VILLAGE_NAMES_LENGTH = 500; + + @Autowired + private VillageUpdateRepository villageUpdateRepository; + + /** + * Replaces one village on a work location mapping and moves the beneficiary + * addresses that user registered under the old village to the new one, in + * a single transaction. + */ + @Override + @Transactional(rollbackFor = Exception.class) + public VillageUpdateResponse updateVillage(VillageUpdateRequest request) throws Exception { + validate(request); + + Object[] mapping = villageUpdateRepository.lockMapping(request.getuSRMappingID()); + if (mapping == null) { + throw new IllegalArgumentException("No work location mapping found with ID " + request.getuSRMappingID()); + } + Integer mappedUserID = ((Number) mapping[0]).intValue(); + if (!mappedUserID.equals(request.getUserID())) { + throw new IllegalArgumentException("Mapping " + request.getuSRMappingID() + " does not belong to user " + + request.getUserID() + ". Reload the user and try again."); + } + if (isTrue(mapping[3])) { + throw new IllegalArgumentException("Mapping " + request.getuSRMappingID() + " is deactivated"); + } + + String userName = villageUpdateRepository.userName(request.getUserID()); + if (userName == null) { + throw new IllegalArgumentException("No user found with ID " + request.getUserID()); + } + + String newVillageName = trimToNull(villageUpdateRepository.villageName(request.getNewVillageID())); + if (newVillageName == null) { + throw new IllegalArgumentException("No active village found with ID " + request.getNewVillageID()); + } + + List villageIDs = split((String) mapping[1]); + List villageNames = split((String) mapping[2]); + String oldVillageID = String.valueOf(request.getOldVillageID()); + String newVillageID = String.valueOf(request.getNewVillageID()); + + int index = villageIDs.indexOf(oldVillageID); + if (index < 0) { + throw new IllegalArgumentException("Village " + oldVillageID + " is no longer mapped to mapping " + + request.getuSRMappingID() + ". Reload the user and try again."); + } + if (villageIDs.contains(newVillageID)) { + throw new IllegalArgumentException("Village " + newVillageName + " is already mapped to this user"); + } + + String oldVillageName = index < villageNames.size() ? villageNames.get(index) : request.getOldVillageName(); + villageIDs.set(index, newVillageID); + while (villageNames.size() < villageIDs.size()) { + villageNames.add(""); + } + villageNames.set(index, newVillageName); + + String joinedIDs = String.join(",", villageIDs); + String joinedNames = String.join(",", villageNames); + if (joinedIDs.length() > MAX_VILLAGE_IDS_LENGTH || joinedNames.length() > MAX_VILLAGE_NAMES_LENGTH) { + throw new IllegalArgumentException( + "Updated village list is too long to store on the mapping (Villageid/VillageName limit)"); + } + + String modifiedBy = trimToNull(request.getModifiedBy()); + villageUpdateRepository.updateMappingVillages(request.getuSRMappingID(), joinedIDs, joinedNames, modifiedBy); + long permanent = villageUpdateRepository.updatePermanentVillage(userName, request.getOldVillageID(), + request.getNewVillageID(), newVillageName, modifiedBy); + long current = villageUpdateRepository.updateCurrentVillage(userName, request.getOldVillageID(), + request.getNewVillageID(), newVillageName, modifiedBy); + + logger.info("Village update complete: uSRMappingID {}, village {} -> {}, addresses perm {} curr {}", + request.getuSRMappingID(), oldVillageID, newVillageID, permanent, current); + + VillageUpdateResponse response = new VillageUpdateResponse(); + response.setuSRMappingID(request.getuSRMappingID()); + response.setUserName(userName); + response.setOldVillageID(request.getOldVillageID()); + response.setOldVillageName(oldVillageName); + response.setNewVillageID(request.getNewVillageID()); + response.setNewVillageName(newVillageName); + response.setPermanentAddressesUpdated(permanent); + response.setCurrentAddressesUpdated(current); + return response; + } + + private void validate(VillageUpdateRequest request) { + if (request == null) { + throw new IllegalArgumentException("Request body is required"); + } + if (request.getUserID() == null || request.getuSRMappingID() == null) { + throw new IllegalArgumentException("User ID and mapping ID are required"); + } + if (request.getOldVillageID() == null || request.getNewVillageID() == null) { + throw new IllegalArgumentException("Old and new village are required"); + } + if (request.getOldVillageID().equals(request.getNewVillageID())) { + throw new IllegalArgumentException("The new village is the same as the old one"); + } + if (trimToNull(request.getModifiedBy()) == null) { + throw new IllegalArgumentException("Modified by is required"); + } + } + + private List split(String csv) { + List values = new ArrayList<>(); + if (csv == null || csv.trim().isEmpty()) { + return values; + } + for (String value : Arrays.asList(csv.split(","))) { + values.add(value.trim()); + } + return values; + } + + private boolean isTrue(Object value) { + if (value instanceof Boolean bool) { + return bool; + } + if (value instanceof Number number) { + return number.intValue() != 0; + } + if (value instanceof byte[] bytes) { + return bytes.length > 0 && bytes[0] != 0; + } + return false; + } + + private String trimToNull(String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; + } +} From f24f4ff77d92b9307d6623735e503e84e6db4162 Mon Sep 17 00:00:00 2001 From: Sneha Date: Wed, 30 Sep 2026 10:42:03 +0530 Subject: [PATCH 10/10] feat: move all beneficiary addresses when user has a single village For a single-village user, updateVillage now moves every address they registered to the new village instead of only rows matching the old village ID. Multi-village users keep the old-village-only behaviour. Adds allAddressesUpdated to the response. Co-Authored-By: Claude Opus 5.5 --- .../villagemapping/VillageUpdateResponse.java | 9 ++++++ .../VillageUpdateRepository.java | 15 ++++++++++ .../VillageUpdateServiceImpl.java | 30 ++++++++++++++----- 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/iemr/admin/model/villagemapping/VillageUpdateResponse.java b/src/main/java/com/iemr/admin/model/villagemapping/VillageUpdateResponse.java index 95ed8a9..7326f47 100644 --- a/src/main/java/com/iemr/admin/model/villagemapping/VillageUpdateResponse.java +++ b/src/main/java/com/iemr/admin/model/villagemapping/VillageUpdateResponse.java @@ -32,6 +32,7 @@ public class VillageUpdateResponse { private long permanentAddressesUpdated; private long currentAddressesUpdated; + private boolean allAddressesUpdated; public Integer getuSRMappingID() { return uSRMappingID; @@ -96,4 +97,12 @@ public long getCurrentAddressesUpdated() { public void setCurrentAddressesUpdated(long currentAddressesUpdated) { this.currentAddressesUpdated = currentAddressesUpdated; } + + public boolean isAllAddressesUpdated() { + return allAddressesUpdated; + } + + public void setAllAddressesUpdated(boolean allAddressesUpdated) { + this.allAddressesUpdated = allAddressesUpdated; + } } diff --git a/src/main/java/com/iemr/admin/repository/villagemapping/VillageUpdateRepository.java b/src/main/java/com/iemr/admin/repository/villagemapping/VillageUpdateRepository.java index 6dbbdd4..273e854 100644 --- a/src/main/java/com/iemr/admin/repository/villagemapping/VillageUpdateRepository.java +++ b/src/main/java/com/iemr/admin/repository/villagemapping/VillageUpdateRepository.java @@ -54,6 +54,11 @@ public class VillageUpdateRepository { "UPDATE db_identity.i_beneficiaryaddress SET CurrVillageId = :newVillageID, CurrVillage = :newVillageName," + " ModifiedBy = :modifiedBy WHERE CreatedBy = :userName AND CurrVillageId = :oldVillageID"; + private static final String SET_ALL_ADDRESS_VILLAGES = + "UPDATE db_identity.i_beneficiaryaddress SET PermVillageId = :newVillageID, PermVillage = :newVillageName," + + " CurrVillageId = :newVillageID, CurrVillage = :newVillageName, ModifiedBy = :modifiedBy" + + " WHERE CreatedBy = :userName"; + @PersistenceContext private EntityManager entityManager; @@ -105,6 +110,16 @@ public long updateCurrentVillage(String userName, Integer oldVillageID, Integer modifiedBy); } + public long updateAllAddressVillages(String userName, Integer newVillageID, String newVillageName, + String modifiedBy) { + Query query = entityManager.createNativeQuery(SET_ALL_ADDRESS_VILLAGES); + query.setParameter("newVillageID", newVillageID); + query.setParameter("newVillageName", newVillageName); + query.setParameter("modifiedBy", modifiedBy); + query.setParameter("userName", userName); + return query.executeUpdate(); + } + private long updateAddressVillage(String sql, String userName, Integer oldVillageID, Integer newVillageID, String newVillageName, String modifiedBy) { Query query = entityManager.createNativeQuery(sql); diff --git a/src/main/java/com/iemr/admin/service/villagemapping/VillageUpdateServiceImpl.java b/src/main/java/com/iemr/admin/service/villagemapping/VillageUpdateServiceImpl.java index a73686b..e90d5cc 100644 --- a/src/main/java/com/iemr/admin/service/villagemapping/VillageUpdateServiceImpl.java +++ b/src/main/java/com/iemr/admin/service/villagemapping/VillageUpdateServiceImpl.java @@ -47,8 +47,8 @@ public class VillageUpdateServiceImpl implements VillageUpdateService { /** * Replaces one village on a work location mapping and moves the beneficiary - * addresses that user registered under the old village to the new one, in - * a single transaction. + * addresses that user registered (i_beneficiaryaddress.CreatedBy) to the new + * village, in a single transaction. */ @Override @Transactional(rollbackFor = Exception.class) @@ -108,13 +108,26 @@ public VillageUpdateResponse updateVillage(VillageUpdateRequest request) throws String modifiedBy = trimToNull(request.getModifiedBy()); villageUpdateRepository.updateMappingVillages(request.getuSRMappingID(), joinedIDs, joinedNames, modifiedBy); - long permanent = villageUpdateRepository.updatePermanentVillage(userName, request.getOldVillageID(), - request.getNewVillageID(), newVillageName, modifiedBy); - long current = villageUpdateRepository.updateCurrentVillage(userName, request.getOldVillageID(), - request.getNewVillageID(), newVillageName, modifiedBy); - logger.info("Village update complete: uSRMappingID {}, village {} -> {}, addresses perm {} curr {}", - request.getuSRMappingID(), oldVillageID, newVillageID, permanent, current); + // A single-village user registered every beneficiary under that village, + // so all of their addresses move. With several villages, only addresses on + // the replaced village move, so the other villages are left as they are. + long permanent; + long current; + boolean allAddresses = villageIDs.size() == 1; + if (allAddresses) { + permanent = villageUpdateRepository.updateAllAddressVillages(userName, request.getNewVillageID(), + newVillageName, modifiedBy); + current = permanent; + } else { + permanent = villageUpdateRepository.updatePermanentVillage(userName, request.getOldVillageID(), + request.getNewVillageID(), newVillageName, modifiedBy); + current = villageUpdateRepository.updateCurrentVillage(userName, request.getOldVillageID(), + request.getNewVillageID(), newVillageName, modifiedBy); + } + + logger.info("Village update complete: uSRMappingID {}, village {} -> {}, allAddresses {}, perm {} curr {}", + request.getuSRMappingID(), oldVillageID, newVillageID, allAddresses, permanent, current); VillageUpdateResponse response = new VillageUpdateResponse(); response.setuSRMappingID(request.getuSRMappingID()); @@ -125,6 +138,7 @@ public VillageUpdateResponse updateVillage(VillageUpdateRequest request) throws response.setNewVillageName(newVillageName); response.setPermanentAddressesUpdated(permanent); response.setCurrentAddressesUpdated(current); + response.setAllAddressesUpdated(allAddresses); return response; }