From 3c5b5b895cd4bc6d1bdddfce596ce9f6054ea0e2 Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Wed, 6 May 2026 12:18:05 +0530 Subject: [PATCH 01/32] add bengali language translation on dynamic forms --- .../iemr/common/data/dynamic_from/FormFieldOption.java | 3 +++ .../com/iemr/common/data/translation/Translation.java | 2 ++ .../service/dynamicForm/FormMasterServiceImpl.java | 9 ++++++++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/iemr/common/data/dynamic_from/FormFieldOption.java b/src/main/java/com/iemr/common/data/dynamic_from/FormFieldOption.java index 8cfeb0de..05f06df2 100644 --- a/src/main/java/com/iemr/common/data/dynamic_from/FormFieldOption.java +++ b/src/main/java/com/iemr/common/data/dynamic_from/FormFieldOption.java @@ -27,6 +27,9 @@ public class FormFieldOption { @Column(name = "label_as") private String labelAs; + @Column(name = "label_bn") + private String labelBn; + @Column(name = "sort_order") private Integer sortOrder; diff --git a/src/main/java/com/iemr/common/data/translation/Translation.java b/src/main/java/com/iemr/common/data/translation/Translation.java index 0dad116d..e0104a53 100644 --- a/src/main/java/com/iemr/common/data/translation/Translation.java +++ b/src/main/java/com/iemr/common/data/translation/Translation.java @@ -20,6 +20,8 @@ public class Translation { private String hindiTranslation; @Column(name = "assamese_translation") private String assameseTranslation; + @Column(name = "bengali_translation") + private String bengaliTranslation; @Column(name = "is_active") private Boolean isActive; } diff --git a/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java b/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java index 0bf1c2fe..38154f4a 100644 --- a/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java +++ b/src/main/java/com/iemr/common/service/dynamicForm/FormMasterServiceImpl.java @@ -160,6 +160,9 @@ public FormResponseDTO getStructuredFormByFormId(String formId, String lang, Str } else if ("en".equalsIgnoreCase(lang)) { translatedLabel = label.getEnglish(); + }else if ("bn".equalsIgnoreCase(lang)) { + translatedLabel = label.getBengaliTranslation(); + } } @@ -171,6 +174,9 @@ public FormResponseDTO getStructuredFormByFormId(String formId, String lang, Str } else if ("en".equalsIgnoreCase(lang)) { translatedPlaceHolder = placeHolder.getEnglish(); + } else if ("bn".equalsIgnoreCase(lang)) { + translatedPlaceHolder = placeHolder.getBengaliTranslation(); + } } @@ -203,7 +209,8 @@ public FormResponseDTO getStructuredFormByFormId(String formId, String lang, Str map.put("value", opt.getValue()); if ("hi".equalsIgnoreCase(lang)) map.put("label", opt.getLabelHi()); else if ("as".equalsIgnoreCase(lang)) map.put("label", opt.getLabelAs()); - else map.put("label", opt.getLabelEn()); + else if("en".equals(lang)) map.put("label", opt.getLabelEn()); + else if("bn".equals(lang)) map.put("label", opt.getLabelBn()); return map; }) .collect(Collectors.toList()); From bc54def0d8c37942baf5b12d65f4ee09a1a3afc0 Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Tue, 12 May 2026 13:17:28 +0530 Subject: [PATCH 02/32] change api version --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b4e8c0d9..ffe74404 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.iemr.common-API common-api - 3.8.1 + 3.8.2 war Common-API From 3e0bbb798eb8b1894858ab7caa438976866fea9b Mon Sep 17 00:00:00 2001 From: SnehaRH <77656297+snehar-nd@users.noreply.github.com> Date: Fri, 22 May 2026 19:05:21 +0530 Subject: [PATCH 03/32] Sn/3.8.1 (#423) * fix: aam-2313 phone number leading with zero - removed zero * fix: allow concurrent sessions for admin, superadmin, and supervisor roles Co-Authored-By: Claude Sonnet 4.6 * fix: added admin and superadmin for the condition --------- Co-authored-by: Claude Sonnet 4.6 --- .../controller/users/IEMRAdminController.java | 52 ++++++++++++++----- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java index 349c3b1e..5ac134cd 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -81,6 +81,7 @@ public class IEMRAdminController { private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); private InputMapper inputMapper = new InputMapper(); + private static final Set CONCURRENT_SESSION_EXEMPT_ROLES = Set.of("admin", "superadmin"); // @Value("${captcha.enable-captcha}") private boolean enableCaptcha =false; @@ -180,11 +181,22 @@ public String userAuthenticate( if (m_User.getUserName() != null && (m_User.getDoLogout() == null || !m_User.getDoLogout()) && (m_User.getWithCredentials() != null && m_User.getWithCredentials())) { - String tokenFromRedis = getConcurrentCheckSessionObjectAgainstUser( - m_User.getUserName().trim().toLowerCase()); - if (tokenFromRedis != null) { - throw new IEMRException( - "You are already logged in,please confirm to logout from other device and login again"); + String userRole = ""; + if (mUser.size() == 1 && mUser.get(0).getM_UserServiceRoleMapping() != null) { + for (UserServiceRoleMapping usrm : mUser.get(0).getM_UserServiceRoleMapping()) { + if (usrm.getM_Role() != null && usrm.getM_Role().getRoleName() != null) { + userRole = usrm.getM_Role().getRoleName(); + break; + } + } + } + if (!CONCURRENT_SESSION_EXEMPT_ROLES.contains(userRole.trim().toLowerCase())) { + String tokenFromRedis = getConcurrentCheckSessionObjectAgainstUser( + m_User.getUserName().trim().toLowerCase()); + if (tokenFromRedis != null) { + throw new IEMRException( + "You are already logged in,please confirm to logout from other device and login again"); + } } } else if (m_User.getUserName() != null && m_User.getDoLogout() != null && m_User.getDoLogout() == true) { deleteSessionObject(m_User.getUserName().trim().toLowerCase()); @@ -412,16 +424,28 @@ public String logOutUserFromConcurrentSession( deleteSessionObjectByGettingSessionDetails(previousTokenFromRedis); sessionObject.deleteSessionObject(previousTokenFromRedis); - // Denylist the active JWT so System 1's requests are immediately rejected - String usernameKey = mUsers.get(0).getUserName().trim().toLowerCase(); - String jtiData = stringRedisTemplate.opsForValue().get("jti:" + usernameKey); - if (jtiData != null) { - String[] parts = jtiData.split("\\|", 2); - tokenDenylist.addTokenToDenylist(parts[0], jwtUtil.getAccessTokenExpiration()); - if (parts.length > 1) { - redisTemplate.delete("user_" + parts[1]); + String userRole = ""; + if (mUsers.get(0).getM_UserServiceRoleMapping() != null) { + for (UserServiceRoleMapping usrm : mUsers.get(0).getM_UserServiceRoleMapping()) { + if (usrm.getM_Role() != null && usrm.getM_Role().getRoleName() != null) { + userRole = usrm.getM_Role().getRoleName(); + break; + } + } + } + if (!CONCURRENT_SESSION_EXEMPT_ROLES.contains(userRole.trim().toLowerCase())) { + // Denylist the active JWT so the first system's requests are immediately rejected + String usernameKey = mUsers.get(0).getUserName().trim().toLowerCase(); + String jtiData = (String) redisTemplate.opsForValue().get("jti:" + usernameKey); + if (jtiData != null) { + String[] parts = jtiData.split("\\|", 2); + String jti = parts[0]; + tokenDenylist.addTokenToDenylist(jti, jwtUtil.getAccessTokenExpiration()); + if (parts.length > 1) { + redisTemplate.delete("user_" + parts[1]); + } + redisTemplate.delete("jti:" + usernameKey); } - stringRedisTemplate.delete("jti:" + usernameKey); } response.setResponse("User successfully logged out"); From cbb18177013dedbed09d83cddcb0414d2aab162d Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Fri, 22 May 2026 20:13:52 +0530 Subject: [PATCH 04/32] fix: use stringRedisTemplate for jti: key read/delete in concurrent session logout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit redisTemplate has Jackson2JsonRedisSerializer as value serializer, so reading the plain-string jti: value caused a deserialization failure (statusCode 5000). jti: keys are written via stringRedisTemplate at login, so reads and deletes must also use stringRedisTemplate — restoring the behaviour from commit 80fa0e5e that was accidentally reverted in #423. Co-Authored-By: Claude Sonnet 4.6 --- .../com/iemr/common/controller/users/IEMRAdminController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java index 5ac134cd..f9942b5e 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -436,7 +436,7 @@ public String logOutUserFromConcurrentSession( if (!CONCURRENT_SESSION_EXEMPT_ROLES.contains(userRole.trim().toLowerCase())) { // Denylist the active JWT so the first system's requests are immediately rejected String usernameKey = mUsers.get(0).getUserName().trim().toLowerCase(); - String jtiData = (String) redisTemplate.opsForValue().get("jti:" + usernameKey); + String jtiData = stringRedisTemplate.opsForValue().get("jti:" + usernameKey); if (jtiData != null) { String[] parts = jtiData.split("\\|", 2); String jti = parts[0]; @@ -444,7 +444,7 @@ public String logOutUserFromConcurrentSession( if (parts.length > 1) { redisTemplate.delete("user_" + parts[1]); } - redisTemplate.delete("jti:" + usernameKey); + stringRedisTemplate.delete("jti:" + usernameKey); } } From 8b548d383988d62fa2354b08f95124fdda643387 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Fri, 22 May 2026 20:48:34 +0530 Subject: [PATCH 05/32] fix: correct exempt roles and allow superadmin concurrent sessions - Fix role name from 'admin' to 'provideradmin' to match actual DB value - Add concurrent session exemption to superUserAuthenticate so SuperAdmin can log in from multiple tabs without being blocked Co-Authored-By: Claude Sonnet 4.6 --- .../controller/users/IEMRAdminController.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java index f9942b5e..f23c1739 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -81,7 +81,7 @@ public class IEMRAdminController { private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); private InputMapper inputMapper = new InputMapper(); - private static final Set CONCURRENT_SESSION_EXEMPT_ROLES = Set.of("admin", "superadmin"); + private static final Set CONCURRENT_SESSION_EXEMPT_ROLES = Set.of("provideradmin", "superadmin"); // @Value("${captcha.enable-captcha}") private boolean enableCaptcha =false; @@ -560,11 +560,13 @@ public String superUserAuthenticate( String refreshToken = null; boolean isMobile = false; if (m_User.getUserName() != null && (m_User.getDoLogout() == null || m_User.getDoLogout() == false)) { - String tokenFromRedis = getConcurrentCheckSessionObjectAgainstUser( - m_User.getUserName().trim().toLowerCase()); - if (tokenFromRedis != null) { - throw new IEMRException( - "You are already logged in,please confirm to logout from other device and login again"); + if (!CONCURRENT_SESSION_EXEMPT_ROLES.contains(m_User.getUserName().trim().toLowerCase())) { + String tokenFromRedis = getConcurrentCheckSessionObjectAgainstUser( + m_User.getUserName().trim().toLowerCase()); + if (tokenFromRedis != null) { + throw new IEMRException( + "You are already logged in,please confirm to logout from other device and login again"); + } } } else if (m_User.getUserName() != null && m_User.getDoLogout() != null && m_User.getDoLogout() == true) { deleteSessionObject(m_User.getUserName().trim().toLowerCase()); From 2c23d9374afc6f3c4d88873a0627fd6b32e80be3 Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Fri, 29 May 2026 12:25:25 +0530 Subject: [PATCH 06/32] Implement feature multiple login attempt --- .../controller/users/IEMRAdminController.java | 89 ++++++++++++++++++- .../service/users/IEMRAdminUserService.java | 2 +- .../users/IEMRAdminUserServiceImpl.java | 5 ++ 3 files changed, 93 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java index 845fe890..ec0298e8 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -171,7 +171,93 @@ public String userAuthenticate( } String decryptPassword = aesUtil.decrypt("Piramal12Piramal", m_User.getPassword()); - List mUser = iemrAdminUserServiceImpl.userAuthenticate(m_User.getUserName(), decryptPassword); + // Fetch user + List existingUser = iemrAdminUserServiceImpl.userExitsCheck(m_User.getUserName()); + + /* + * ========================================= + * ACCOUNT LOCK CHECK + * ========================================= + */ + if(!existingUser.isEmpty()){ + if (existingUser.get(0) != null + && existingUser.get(0).getFailedAttempt() != null + && existingUser.get(0).getFailedAttempt() >= 5) { + + throw new IEMRException( + "Your account has been locked due to multiple failed login attempts. Please contact administrator."); + } + } + + + List mUser = iemrAdminUserServiceImpl + .userAuthenticate(m_User.getUserName(), decryptPassword); + + /* + * ========================================= + * FAILED LOGIN ATTEMPT LOGIC + * ========================================= + */ + if (mUser == null || mUser.isEmpty()) { + if(!existingUser.isEmpty()){ + if (existingUser != null) { + + Integer failedAttempt = existingUser.get(0).getFailedAttempt() != null + ? existingUser.get(0).getFailedAttempt() + : 0; + + failedAttempt++; + + existingUser.get(0).setFailedAttempt(failedAttempt); + + iemrAdminUserServiceImpl.save(existingUser.get(0)); + + int remainingAttempts = 5 - failedAttempt; + + // Lock account on 5th attempt + if (failedAttempt >= 5) { + + + + response.setError(new IEMRException( + "Your account has been locked due to multiple failed login attempts.")); + return response.toString(); + } + + // Warning on 3rd attempt + if (failedAttempt == 4) { + + + response.setError(new IEMRException( + "Invalid username or password. Remaining attempts: " + + remainingAttempts + + ". If you enter wrong username or password again, your account will be locked.")); + return response.toString(); + } + + + response.setError(new IEMRException( + "Invalid username or password. Remaining attempts: " + + remainingAttempts)); + return response.toString(); + + } + } + + + throw new IEMRException("Invalid username or password."); + } + + /* + * ========================================= + * RESET FAILED ATTEMPTS ON SUCCESS LOGIN + * ========================================= + */ + User loggedInUser = mUser.get(0); + + loggedInUser.setFailedAttempt(0); + + iemrAdminUserServiceImpl.save(loggedInUser); JSONObject resMap = new JSONObject(); JSONObject serviceRoleMultiMap = new JSONObject(); JSONObject serviceRoleMap = new JSONObject(); @@ -253,7 +339,6 @@ public String userAuthenticate( // Facility data for ALL users - common pattern, empty if not applicable try { if (mUser.size() == 1) { - User loggedInUser = mUser.get(0); String userRoleName = ""; if (loggedInUser.getM_UserServiceRoleMapping() != null) { for (UserServiceRoleMapping usrm : loggedInUser.getM_UserServiceRoleMapping()) { diff --git a/src/main/java/com/iemr/common/service/users/IEMRAdminUserService.java b/src/main/java/com/iemr/common/service/users/IEMRAdminUserService.java index 26b7bb15..1db05322 100644 --- a/src/main/java/com/iemr/common/service/users/IEMRAdminUserService.java +++ b/src/main/java/com/iemr/common/service/users/IEMRAdminUserService.java @@ -126,5 +126,5 @@ public List getUserServiceRoleMappingForProvider(Integ List findUserIdByUserName(String userName) throws IEMRException; - + User save(User loggedInUser); } diff --git a/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java b/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java index 71d72c97..3d20be1b 100644 --- a/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java +++ b/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java @@ -1230,4 +1230,9 @@ public List findUserIdByUserName(String userName) { return iEMRUserRepositoryCustom.findUserName(userName); } + + @Override + public User save(User loggedInUser) { + return iEMRUserRepositoryCustom.save(loggedInUser); + } } From fe3336a170ac1bf3d2271d26665fc5888b5dbfdc Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Fri, 29 May 2026 13:09:15 +0530 Subject: [PATCH 07/32] Implement feature multiple login attempt --- .../controller/users/IEMRAdminController.java | 61 +++++++++---------- 1 file changed, 28 insertions(+), 33 deletions(-) diff --git a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java index ec0298e8..b3644743 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -198,54 +198,49 @@ public String userAuthenticate( * FAILED LOGIN ATTEMPT LOGIC * ========================================= */ - if (mUser == null || mUser.isEmpty()) { - if(!existingUser.isEmpty()){ - if (existingUser != null) { - - Integer failedAttempt = existingUser.get(0).getFailedAttempt() != null - ? existingUser.get(0).getFailedAttempt() - : 0; - - failedAttempt++; + if(!existingUser.isEmpty()){ + if (existingUser != null) { - existingUser.get(0).setFailedAttempt(failedAttempt); + Integer failedAttempt = existingUser.get(0).getFailedAttempt() != null + ? existingUser.get(0).getFailedAttempt() + : 0; - iemrAdminUserServiceImpl.save(existingUser.get(0)); + failedAttempt++; - int remainingAttempts = 5 - failedAttempt; + existingUser.get(0).setFailedAttempt(failedAttempt); - // Lock account on 5th attempt - if (failedAttempt >= 5) { + iemrAdminUserServiceImpl.save(existingUser.get(0)); + int remainingAttempts = 5 - failedAttempt; + // Lock account on 5th attempt + if (failedAttempt >= 5) { - response.setError(new IEMRException( - "Your account has been locked due to multiple failed login attempts.")); - return response.toString(); - } - // Warning on 3rd attempt - if (failedAttempt == 4) { + response.setError(new IEMRException( + "Your account has been locked due to multiple failed login attempts.")); + return response.toString(); + } - response.setError(new IEMRException( - "Invalid username or password. Remaining attempts: " - + remainingAttempts - + ". If you enter wrong username or password again, your account will be locked.")); - return response.toString(); - } + // Warning on 3rd attempt + if (failedAttempt == 4) { - response.setError(new IEMRException( - "Invalid username or password. Remaining attempts: " - + remainingAttempts)); - return response.toString(); + response.setError(new IEMRException( + "Invalid username or password. Remaining attempts: " + + remainingAttempts + + ". If you enter wrong username or password again, your account will be locked.")); + return response.toString(); + } - } - } + response.setError(new IEMRException( + "Invalid username or password. Remaining attempts: " + + remainingAttempts)); + return response.toString(); - throw new IEMRException("Invalid username or password."); + } } /* From a6a6d3c4814db3d45c3d3f3e88bdbb5fd6c1adeb Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Fri, 12 Jun 2026 13:56:13 +0530 Subject: [PATCH 08/32] fix: delete camp:vanID and camp:parkingPlaceID from Redis on MMU logout Redis keys were stored on MMU login with 30-day TTL but never deleted on logout. Added cleanup in userLogout() so stale camp config does not persist after the MMU session ends. Co-Authored-By: Claude Sonnet 4.6 --- .../iemr/common/controller/users/IEMRAdminController.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java index f23c1739..a82374a5 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -1018,6 +1018,13 @@ public String userLogout(HttpServletRequest request) { try { deleteSessionObjectByGettingSessionDetails(request.getHeader("Authorization")); sessionObject.deleteSessionObject(request.getHeader("Authorization")); + try { + stringRedisTemplate.delete("camp:vanID"); + stringRedisTemplate.delete("camp:parkingPlaceID"); + logger.info("Camp config cleared from Redis on MMU logout"); + } catch (Exception redisEx) { + logger.warn("Failed to clear camp Redis keys on logout: {}", redisEx.getMessage()); + } response.setResponse("Success"); } catch (Exception e) { response.setError(e); From 758bf3674694bad1177f15cb5a7f95fb5986c2f1 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Tue, 16 Jun 2026 15:11:41 +0530 Subject: [PATCH 09/32] feat: add endpoint to expose server's LAN IP address Lets a device on the same network discover the API's LAN IP and port without needing it typed in manually, and bumps version to 3.8.2. --- pom.xml | 2 +- .../controller/connect/ConnectController.java | 57 +++++++++++++++++++ .../com/iemr/common/utils/NetworkUtil.java | 56 ++++++++++++++++++ 3 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/iemr/common/controller/connect/ConnectController.java create mode 100644 src/main/java/com/iemr/common/utils/NetworkUtil.java diff --git a/pom.xml b/pom.xml index b4e8c0d9..ffe74404 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.iemr.common-API common-api - 3.8.1 + 3.8.2 war Common-API diff --git a/src/main/java/com/iemr/common/controller/connect/ConnectController.java b/src/main/java/com/iemr/common/controller/connect/ConnectController.java new file mode 100644 index 00000000..7ecf45ba --- /dev/null +++ b/src/main/java/com/iemr/common/controller/connect/ConnectController.java @@ -0,0 +1,57 @@ +/* +* 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.common.controller.connect; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.iemr.common.utils.NetworkUtil; + +import io.swagger.v3.oas.annotations.Operation; + +/** + * Exposes the server's LAN address so a mobile device on the same wifi + * network can connect to this API. + */ +@RestController +@RequestMapping(value = "/public/connect") +public class ConnectController { + + @Value("${server.port:8080}") + private int serverPort; + + @Operation(summary = "Get the server's LAN IP address and port") + @GetMapping(value = "/info", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getConnectInfo() { + Map response = new LinkedHashMap<>(); + response.put("ip", NetworkUtil.getLanIPAddress()); + response.put("port", serverPort); + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/com/iemr/common/utils/NetworkUtil.java b/src/main/java/com/iemr/common/utils/NetworkUtil.java new file mode 100644 index 00000000..b5ded4d5 --- /dev/null +++ b/src/main/java/com/iemr/common/utils/NetworkUtil.java @@ -0,0 +1,56 @@ +/* +* 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.common.utils; + +import java.net.DatagramSocket; +import java.net.InetAddress; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Resolves the single LAN-facing IPv4 address the OS would use for outbound + * traffic, used to build a URL that a device on the same wifi network (e.g. + * a mobile phone) can connect to. A machine can have several network + * interfaces (wifi, ethernet, VPN, virtual adapters); connecting a UDP + * socket to an external address - without sending any packet - makes the OS + * routing table pick the one real outbound interface, avoiding ambiguity + * from iterating all interfaces. + */ +public final class NetworkUtil { + + private static final Logger logger = LoggerFactory.getLogger(NetworkUtil.class); + private static final String FALLBACK_IP = "127.0.0.1"; + + private NetworkUtil() { + } + + public static String getLanIPAddress() { + try (DatagramSocket socket = new DatagramSocket()) { + socket.connect(InetAddress.getByName("8.8.8.8"), 10002); + return socket.getLocalAddress().getHostAddress(); + } catch (Exception e) { + logger.error("Failed to resolve LAN IP address", e); + return FALLBACK_IP; + } + } +} From be92d7c5cbbd993b569213194eb691e803ef91d0 Mon Sep 17 00:00:00 2001 From: snehar-nd Date: Tue, 16 Jun 2026 17:14:44 +0530 Subject: [PATCH 10/32] fix: extented the token expiry timing --- src/main/java/com/iemr/common/utils/CookieUtil.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/iemr/common/utils/CookieUtil.java b/src/main/java/com/iemr/common/utils/CookieUtil.java index 92c071c5..1562e3c4 100644 --- a/src/main/java/com/iemr/common/utils/CookieUtil.java +++ b/src/main/java/com/iemr/common/utils/CookieUtil.java @@ -35,8 +35,8 @@ public void addJwtTokenToCookie(String Jwttoken, HttpServletResponse response, H // Make the cookie HttpOnly to prevent JavaScript access for security cookie.setHttpOnly(true); - // Set the Max-Age (expiry time) in seconds (8 hours) - cookie.setMaxAge(60 * 60 * 8); // 8 hours expiration + // Set the Max-Age (expiry time) in seconds (10 hours) + cookie.setMaxAge(60 * 60 * 10); // 10 hours expiration // Set the path to "/" so the cookie is available across the entire application cookie.setPath("/"); From e04714b8c97315440bde52955a8a7ecfd8211ec8 Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Mon, 22 Jun 2026 15:26:27 +0530 Subject: [PATCH 11/32] fix issue of feature multiple login attempt --- .../users/IEMRAdminUserServiceImpl.java | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java b/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java index 3d20be1b..13adda14 100644 --- a/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java +++ b/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java @@ -265,12 +265,28 @@ public List userAuthenticate(String userName, String password) throws Exce checkUserAccountStatus(user); iEMRUserRepositoryCustom.save(user); } else if (validatePassword == 0) { - if (user.getFailedAttempt() + 1 < failedAttempt) { - user.setFailedAttempt(user.getFailedAttempt() + 1); + int currentFailedAttempt = + user.getFailedAttempt() != null ? user.getFailedAttempt() : 0; + + int newFailedAttempt = currentFailedAttempt + 1; + int remainingAttempts = failedAttempt - newFailedAttempt; + if (newFailedAttempt < failedAttempt) { + + user.setFailedAttempt(newFailedAttempt); user = iEMRUserRepositoryCustom.save(user); + logger.warn("User Password Wrong"); - throw new IEMRException("Invalid username or password"); - } else if (user.getFailedAttempt() + 1 >= failedAttempt) { + + if (remainingAttempts == 1) { + throw new IEMRException( + "Invalid username or password. Remaining attempts: 1. " + + "If you enter wrong username or password again, your account will be locked."); + } + + throw new IEMRException( + "Invalid username or password. Remaining attempts: " + + remainingAttempts); + }else if (user.getFailedAttempt() + 1 >= failedAttempt) { user.setFailedAttempt(user.getFailedAttempt() + 1); user.setDeleted(true); user = iEMRUserRepositoryCustom.save(user); @@ -278,14 +294,14 @@ public List userAuthenticate(String userName, String password) throws Exce ConfigProperties.getInteger("failedLoginAttempt")); throw new IEMRException( - "Invalid username or password. Please contact administrator."); + "Your account has been locked due to multiple failed login attempts. Please contact administrator."); } else { user.setFailedAttempt(user.getFailedAttempt() + 1); user = iEMRUserRepositoryCustom.save(user); logger.warn("Failed login attempt {} of {} for a user account.", user.getFailedAttempt(), ConfigProperties.getInteger("failedLoginAttempt")); throw new IEMRException( - "Invalid username or password. Please contact administrator."); + "Your account has been locked due to multiple failed login attempts. Please contact administrator."); } } else { checkUserAccountStatus(user); From 818257c3535b4dff2c26aa0a4af200bec635b0fd Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Mon, 22 Jun 2026 16:44:01 +0530 Subject: [PATCH 12/32] fix issue of feature multiple login attempt --- .../users/IEMRAdminUserServiceImpl.java | 28 ++++--------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java b/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java index 13adda14..3d20be1b 100644 --- a/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java +++ b/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java @@ -265,28 +265,12 @@ public List userAuthenticate(String userName, String password) throws Exce checkUserAccountStatus(user); iEMRUserRepositoryCustom.save(user); } else if (validatePassword == 0) { - int currentFailedAttempt = - user.getFailedAttempt() != null ? user.getFailedAttempt() : 0; - - int newFailedAttempt = currentFailedAttempt + 1; - int remainingAttempts = failedAttempt - newFailedAttempt; - if (newFailedAttempt < failedAttempt) { - - user.setFailedAttempt(newFailedAttempt); + if (user.getFailedAttempt() + 1 < failedAttempt) { + user.setFailedAttempt(user.getFailedAttempt() + 1); user = iEMRUserRepositoryCustom.save(user); - logger.warn("User Password Wrong"); - - if (remainingAttempts == 1) { - throw new IEMRException( - "Invalid username or password. Remaining attempts: 1. " - + "If you enter wrong username or password again, your account will be locked."); - } - - throw new IEMRException( - "Invalid username or password. Remaining attempts: " - + remainingAttempts); - }else if (user.getFailedAttempt() + 1 >= failedAttempt) { + throw new IEMRException("Invalid username or password"); + } else if (user.getFailedAttempt() + 1 >= failedAttempt) { user.setFailedAttempt(user.getFailedAttempt() + 1); user.setDeleted(true); user = iEMRUserRepositoryCustom.save(user); @@ -294,14 +278,14 @@ public List userAuthenticate(String userName, String password) throws Exce ConfigProperties.getInteger("failedLoginAttempt")); throw new IEMRException( - "Your account has been locked due to multiple failed login attempts. Please contact administrator."); + "Invalid username or password. Please contact administrator."); } else { user.setFailedAttempt(user.getFailedAttempt() + 1); user = iEMRUserRepositoryCustom.save(user); logger.warn("Failed login attempt {} of {} for a user account.", user.getFailedAttempt(), ConfigProperties.getInteger("failedLoginAttempt")); throw new IEMRException( - "Your account has been locked due to multiple failed login attempts. Please contact administrator."); + "Invalid username or password. Please contact administrator."); } } else { checkUserAccountStatus(user); From a27db85fa8bf9c5324245b756fff31cd5ea4ec61 Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Mon, 22 Jun 2026 17:07:04 +0530 Subject: [PATCH 13/32] fix issue of feature multiple login attempt --- .../controller/users/IEMRAdminController.java | 71 ------------------- .../users/IEMRAdminUserServiceImpl.java | 28 ++++++-- 2 files changed, 22 insertions(+), 77 deletions(-) diff --git a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java index b3644743..f3612fdb 100644 --- a/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java +++ b/src/main/java/com/iemr/common/controller/users/IEMRAdminController.java @@ -171,83 +171,12 @@ public String userAuthenticate( } String decryptPassword = aesUtil.decrypt("Piramal12Piramal", m_User.getPassword()); - // Fetch user - List existingUser = iemrAdminUserServiceImpl.userExitsCheck(m_User.getUserName()); - - /* - * ========================================= - * ACCOUNT LOCK CHECK - * ========================================= - */ - if(!existingUser.isEmpty()){ - if (existingUser.get(0) != null - && existingUser.get(0).getFailedAttempt() != null - && existingUser.get(0).getFailedAttempt() >= 5) { - - throw new IEMRException( - "Your account has been locked due to multiple failed login attempts. Please contact administrator."); - } - } List mUser = iemrAdminUserServiceImpl .userAuthenticate(m_User.getUserName(), decryptPassword); - /* - * ========================================= - * FAILED LOGIN ATTEMPT LOGIC - * ========================================= - */ - if(!existingUser.isEmpty()){ - if (existingUser != null) { - - Integer failedAttempt = existingUser.get(0).getFailedAttempt() != null - ? existingUser.get(0).getFailedAttempt() - : 0; - - failedAttempt++; - - existingUser.get(0).setFailedAttempt(failedAttempt); - - iemrAdminUserServiceImpl.save(existingUser.get(0)); - - int remainingAttempts = 5 - failedAttempt; - - // Lock account on 5th attempt - if (failedAttempt >= 5) { - - - - response.setError(new IEMRException( - "Your account has been locked due to multiple failed login attempts.")); - return response.toString(); - } - - // Warning on 3rd attempt - if (failedAttempt == 4) { - - - response.setError(new IEMRException( - "Invalid username or password. Remaining attempts: " - + remainingAttempts - + ". If you enter wrong username or password again, your account will be locked.")); - return response.toString(); - } - - - response.setError(new IEMRException( - "Invalid username or password. Remaining attempts: " - + remainingAttempts)); - return response.toString(); - - } - } - /* - * ========================================= - * RESET FAILED ATTEMPTS ON SUCCESS LOGIN - * ========================================= - */ User loggedInUser = mUser.get(0); loggedInUser.setFailedAttempt(0); diff --git a/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java b/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java index 3d20be1b..13adda14 100644 --- a/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java +++ b/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java @@ -265,12 +265,28 @@ public List userAuthenticate(String userName, String password) throws Exce checkUserAccountStatus(user); iEMRUserRepositoryCustom.save(user); } else if (validatePassword == 0) { - if (user.getFailedAttempt() + 1 < failedAttempt) { - user.setFailedAttempt(user.getFailedAttempt() + 1); + int currentFailedAttempt = + user.getFailedAttempt() != null ? user.getFailedAttempt() : 0; + + int newFailedAttempt = currentFailedAttempt + 1; + int remainingAttempts = failedAttempt - newFailedAttempt; + if (newFailedAttempt < failedAttempt) { + + user.setFailedAttempt(newFailedAttempt); user = iEMRUserRepositoryCustom.save(user); + logger.warn("User Password Wrong"); - throw new IEMRException("Invalid username or password"); - } else if (user.getFailedAttempt() + 1 >= failedAttempt) { + + if (remainingAttempts == 1) { + throw new IEMRException( + "Invalid username or password. Remaining attempts: 1. " + + "If you enter wrong username or password again, your account will be locked."); + } + + throw new IEMRException( + "Invalid username or password. Remaining attempts: " + + remainingAttempts); + }else if (user.getFailedAttempt() + 1 >= failedAttempt) { user.setFailedAttempt(user.getFailedAttempt() + 1); user.setDeleted(true); user = iEMRUserRepositoryCustom.save(user); @@ -278,14 +294,14 @@ public List userAuthenticate(String userName, String password) throws Exce ConfigProperties.getInteger("failedLoginAttempt")); throw new IEMRException( - "Invalid username or password. Please contact administrator."); + "Your account has been locked due to multiple failed login attempts. Please contact administrator."); } else { user.setFailedAttempt(user.getFailedAttempt() + 1); user = iEMRUserRepositoryCustom.save(user); logger.warn("Failed login attempt {} of {} for a user account.", user.getFailedAttempt(), ConfigProperties.getInteger("failedLoginAttempt")); throw new IEMRException( - "Invalid username or password. Please contact administrator."); + "Your account has been locked due to multiple failed login attempts. Please contact administrator."); } } else { checkUserAccountStatus(user); From c3013e0a0b46c08369ee7ca6595673df9a1b79b4 Mon Sep 17 00:00:00 2001 From: Saurav Mishra <80103738+SauravBizbRolly@users.noreply.github.com> Date: Fri, 26 Jun 2026 15:11:36 +0530 Subject: [PATCH 14/32] Increase timing of refresh token (#435) --- src/main/resources/application.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 4a2de342..bc7a8c14 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -177,7 +177,7 @@ account.lock.duration.hours=24 #Jwt Token configuration jwt.access.expiration=28800000 -jwt.refresh.expiration=604800000 +jwt.refresh.expiration=7776000000 # local env From 16dc5af4306f3f1bfc52041943fd810323a8ff54 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Fri, 26 Jun 2026 15:29:24 +0530 Subject: [PATCH 15/32] fix: restore remaining-attempts lockout message from PR #431 Saurav's PR #431 added a "Remaining attempts: N" message on failed login to warn users before account lockout. Port this into the refactored handleFailedLoginAttempt helper so both userAuthenticate and superUserAuthenticate keep the behavior after merging release-3.8.1's account-lock refactor into release-3.8.2. --- .../common/service/users/IEMRAdminUserServiceImpl.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java b/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java index d8d4abe8..6b5dabf6 100644 --- a/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java +++ b/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java @@ -308,7 +308,13 @@ private void handleFailedLoginAttempt(User user, int failedAttemptThreshold) thr user.setFailedAttempt(currentAttempts + 1); iEMRUserRepositoryCustom.save(user); logger.warn("User Password Wrong"); - throw new IEMRException("Invalid username or password"); + int remainingAttempts = failedAttemptThreshold - (currentAttempts + 1); + if (remainingAttempts == 1) { + throw new IEMRException( + "Invalid username or password. Remaining attempts: 1. " + + "If you enter wrong username or password again, your account will be locked."); + } + throw new IEMRException("Invalid username or password. Remaining attempts: " + remainingAttempts); } else { java.sql.Timestamp lockTime = new java.sql.Timestamp(System.currentTimeMillis()); user.setFailedAttempt(currentAttempts + 1); From 865c266dca92ad24829421c46bd2c1d494cc59ac Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Fri, 26 Jun 2026 16:04:16 +0530 Subject: [PATCH 16/32] fix: restore account-lockout message from PR #432 The merge of release-3.8.1's account-lock refactor into release-3.8.2 replaced Saurav Mishra's inline multiple-login-attempt logic (PR #426/#431/#432) with the refactored handlePasswordValidationAndLocking helper. The refactor kept the "Remaining attempts" warning but dropped the final lockout message text, falling back to the unrelated generateLockoutErrorMessage used for already-locked accounts. Restore the exact lockout message from PR #432 so the refactored helper preserves both branches' intended behavior. --- .../iemr/common/service/users/IEMRAdminUserServiceImpl.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java b/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java index 6b5dabf6..e411b9ef 100644 --- a/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java +++ b/src/main/java/com/iemr/common/service/users/IEMRAdminUserServiceImpl.java @@ -323,7 +323,8 @@ private void handleFailedLoginAttempt(User user, int failedAttemptThreshold) thr iEMRUserRepositoryCustom.save(user); logger.warn("User Account has been locked after reaching the limit of {} failed login attempts.", failedAttemptThreshold); - throw new IEMRException(generateLockoutErrorMessage(lockTime)); + throw new IEMRException( + "Your account has been locked due to multiple failed login attempts. Please contact administrator."); } } From d2efa54b304f77ba23757a3abdd0e795c8d0372c Mon Sep 17 00:00:00 2001 From: SnehaRH <77656297+snehar-nd@users.noreply.github.com> Date: Mon, 29 Jun 2026 13:04:10 +0530 Subject: [PATCH 17/32] fix: AMM-2343 on lock/Unlock the api was returning 500 error (#437) Co-authored-by: Sneha --- .../java/com/iemr/common/utils/JwtUserIdValidationFilter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java b/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java index 557d5da5..3be36bbf 100644 --- a/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java +++ b/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java @@ -168,7 +168,7 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo logger.info("Validating JWT token from cookie"); if (jwtAuthenticationUtil.validateUserIdAndJwtToken(jwtFromCookie)) { AuthorizationHeaderRequestWrapper authorizationHeaderRequestWrapper = new AuthorizationHeaderRequestWrapper( - request, ""); + request, authHeader != null ? authHeader : ""); filterChain.doFilter(authorizationHeaderRequestWrapper, servletResponse); return; } @@ -176,7 +176,7 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo logger.info("Validating JWT token from header"); if (jwtAuthenticationUtil.validateUserIdAndJwtToken(jwtFromHeader)) { AuthorizationHeaderRequestWrapper authorizationHeaderRequestWrapper = new AuthorizationHeaderRequestWrapper( - request, ""); + request, authHeader != null ? authHeader : ""); filterChain.doFilter(authorizationHeaderRequestWrapper, servletResponse); return; } From 01a5f5fb376f5d3a9c6ac6689f52c0d6e36904e1 Mon Sep 17 00:00:00 2001 From: Vishwanath Balkur <118195001+vishwab1@users.noreply.github.com> Date: Wed, 22 Jul 2026 17:57:03 +0530 Subject: [PATCH 18/32] Merge StopTB-related fixes from release-3.8.3 into release-3.8.2 (#442) * fix(health): stop reporting MySQL as DOWN when SELECT 1 succeeds checkDatabaseConnectivity() was deriving the status field from the background diagnostic severity (pool usage, long transactions, deadlocks) even after the live SELECT 1 connectivity check succeeded - so a CRITICAL finding from the background scan (e.g. connection pool >95% full) caused /common-api/health to report mysql.status=DOWN despite the database being fully reachable and serving real queries (confirmed: login worked fine while health reported DOWN). status now reflects connectivity only (UP if SELECT 1 succeeds, DOWN only on an actual connection failure); severity continues to surface the background diagnostic findings independently, so capacity/performance warnings are still visible without being misreported as an outage. Removed resolveDatabaseStatus() and the now-unused STATUS_DEGRADED constant, both made dead by this change. Co-Authored-By: Claude Sonnet 4.6 * feat(beneficiary): enhance GPS data handling in beneficiary demographics and address flows * Update `gpsTimestamp` in `BeneficiaryDemographicsModel` and `Address` from `Timestamp` to `Long` to support epoch-millisecond values received from clients. * Add missing GPS-related fields (`latitude`, `longitude`, `digipin`, `isGpsUnavailable`, and `gpsUnavailableReason`) to the `Address` DTO for complete GPS data transfer. * Extend mapper decorators (`BenCompleteDetailMapperDecorator`, `CommonIdentityMapperDecorator`, and `IdentityBenEditMapperDecorator`) to propagate GPS information across beneficiary and identity workflows. * fix: add missing $ to resolve GEN_BENEFICIARY_IDS_API_URL in docker properties Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claude Sonnet 4.6 Co-authored-by: Sehjot Singh Pannu --- src/main/environment/common_docker.properties | 2 +- .../java/com/iemr/common/dto/identity/Address.java | 12 ++++++++++++ .../mapper/BenCompleteDetailMapperDecorator.java | 12 ++++++++++++ .../mapper/CommonIdentityMapperDecorator.java | 6 ++++++ .../mapper/IdentityBenEditMapperDecorator.java | 6 ++++++ .../beneficiary/BeneficiaryDemographicsModel.java | 13 +++++++++++++ .../iemr/common/service/health/HealthService.java | 14 ++++---------- 7 files changed, 54 insertions(+), 11 deletions(-) diff --git a/src/main/environment/common_docker.properties b/src/main/environment/common_docker.properties index 064090b3..aca7345a 100644 --- a/src/main/environment/common_docker.properties +++ b/src/main/environment/common_docker.properties @@ -218,4 +218,4 @@ otp.ratelimit.enabled=${OTP_RATELIMIT_ENABLED} otp.ratelimit.minute-limit=${OTP_RATELIMIT_MINUTE_LIMIT} otp.ratelimit.hour-limit=${OTP_RATELIMIT_HOUR_LIMIT} otp.ratelimit.day-limit=${OTP_RATELIMIT_DAY_LIMIT} -generateBeneficiaryIDs-api-url={GEN_BENEFICIARY_IDS_API_URL} +generateBeneficiaryIDs-api-url=${GEN_BENEFICIARY_IDS_API_URL} diff --git a/src/main/java/com/iemr/common/dto/identity/Address.java b/src/main/java/com/iemr/common/dto/identity/Address.java index 7a5e563f..b857f21d 100644 --- a/src/main/java/com/iemr/common/dto/identity/Address.java +++ b/src/main/java/com/iemr/common/dto/identity/Address.java @@ -52,6 +52,12 @@ private String parkingPlaceName; private Integer servicePointID; private String servicePointName; + private Double gpsLatitude; + private Double gpsLongitude; + private String digipin; + private Long gpsTimestamp; + private Boolean isGpsUnavailable; + private String gpsUnavailableReason; public static Address bendemographicsAddressMapper(BeneficiaryDemographicsModel i_bendemographics) { Address address = new Address(); @@ -87,6 +93,12 @@ public static Address bendemographicsAddressMapper(BeneficiaryDemographicsModel address.setSubDistrictId(i_bendemographics.getBlockID()); address.setVillageId(i_bendemographics.getDistrictBranchID()); address.setPinCode(i_bendemographics.getPinCode()); + address.setGpsLatitude(i_bendemographics.getLatitude()); + address.setGpsLongitude(i_bendemographics.getLongitude()); + address.setDigipin(i_bendemographics.getDigipin()); + address.setGpsTimestamp(i_bendemographics.getGpsTimestamp()); + address.setIsGpsUnavailable(i_bendemographics.getIsGpsUnavailable()); + address.setGpsUnavailableReason(i_bendemographics.getGpsUnavailableReason()); return address; } diff --git a/src/main/java/com/iemr/common/mapper/BenCompleteDetailMapperDecorator.java b/src/main/java/com/iemr/common/mapper/BenCompleteDetailMapperDecorator.java index 7d263b25..8b29ec15 100644 --- a/src/main/java/com/iemr/common/mapper/BenCompleteDetailMapperDecorator.java +++ b/src/main/java/com/iemr/common/mapper/BenCompleteDetailMapperDecorator.java @@ -158,6 +158,12 @@ public BeneficiaryDemographicsModel createBenDemographicsModel(BeneficiariesDTO demographicsModel.setServicePointID(address.getServicePointID()); demographicsModel.setServicePointName(address.getServicePointName()); demographicsModel.setHabitation(address.getHabitation()); + demographicsModel.setLatitude(address.getGpsLatitude()); + demographicsModel.setLongitude(address.getGpsLongitude()); + demographicsModel.setDigipin(address.getDigipin()); + demographicsModel.setGpsTimestamp(address.getGpsTimestamp()); + demographicsModel.setIsGpsUnavailable(address.getIsGpsUnavailable()); + demographicsModel.setGpsUnavailableReason(address.getGpsUnavailableReason()); } demographicsModel.setHealthCareWorkerID(benDetails.getHealthCareWorkerId()); demographicsModel.setPreferredLangID(beneficiariesDTO.getBeneficiaryDetails().getPreferredLanguageId()); @@ -216,6 +222,12 @@ protected Address beneficiaryDemographicsModelToAddress(BeneficiaryDemographicsM address.setSubDistrict( blockMapper.districtBlockToModelByID(beneficiaryDemographicsModel.getBlockID()).getBlockName()); } + address.setGpsLatitude(beneficiaryDemographicsModel.getLatitude()); + address.setGpsLongitude(beneficiaryDemographicsModel.getLongitude()); + address.setDigipin(beneficiaryDemographicsModel.getDigipin()); + address.setGpsTimestamp(beneficiaryDemographicsModel.getGpsTimestamp()); + address.setIsGpsUnavailable(beneficiaryDemographicsModel.getIsGpsUnavailable()); + address.setGpsUnavailableReason(beneficiaryDemographicsModel.getGpsUnavailableReason()); return address; } diff --git a/src/main/java/com/iemr/common/mapper/CommonIdentityMapperDecorator.java b/src/main/java/com/iemr/common/mapper/CommonIdentityMapperDecorator.java index 1480f306..f18920ad 100644 --- a/src/main/java/com/iemr/common/mapper/CommonIdentityMapperDecorator.java +++ b/src/main/java/com/iemr/common/mapper/CommonIdentityMapperDecorator.java @@ -248,6 +248,12 @@ protected Address beneficiaryDemographicsModelToAddress(BeneficiaryDemographicsM address.setSubDistrict( blockMapper.districtBlockToModelByID(beneficiaryDemographicsModel.getBlockID()).getBlockName()); } + address.setGpsLatitude(beneficiaryDemographicsModel.getLatitude()); + address.setGpsLongitude(beneficiaryDemographicsModel.getLongitude()); + address.setDigipin(beneficiaryDemographicsModel.getDigipin()); + address.setGpsTimestamp(beneficiaryDemographicsModel.getGpsTimestamp()); + address.setIsGpsUnavailable(beneficiaryDemographicsModel.getIsGpsUnavailable()); + address.setGpsUnavailableReason(beneficiaryDemographicsModel.getGpsUnavailableReason()); return address; } diff --git a/src/main/java/com/iemr/common/mapper/IdentityBenEditMapperDecorator.java b/src/main/java/com/iemr/common/mapper/IdentityBenEditMapperDecorator.java index e0f3ef1c..27b4a602 100644 --- a/src/main/java/com/iemr/common/mapper/IdentityBenEditMapperDecorator.java +++ b/src/main/java/com/iemr/common/mapper/IdentityBenEditMapperDecorator.java @@ -107,6 +107,12 @@ protected Address beneficiaryDemographicsModelToAddress(BeneficiaryDemographicsM address.setSubDistrict( blockMapper.districtBlockToModelByID(beneficiaryDemographicsModel.getBlockID()).getBlockName()); } + address.setGpsLatitude(beneficiaryDemographicsModel.getLatitude()); + address.setGpsLongitude(beneficiaryDemographicsModel.getLongitude()); + address.setDigipin(beneficiaryDemographicsModel.getDigipin()); + address.setGpsTimestamp(beneficiaryDemographicsModel.getGpsTimestamp()); + address.setIsGpsUnavailable(beneficiaryDemographicsModel.getIsGpsUnavailable()); + address.setGpsUnavailableReason(beneficiaryDemographicsModel.getGpsUnavailableReason()); return address; } diff --git a/src/main/java/com/iemr/common/model/beneficiary/BeneficiaryDemographicsModel.java b/src/main/java/com/iemr/common/model/beneficiary/BeneficiaryDemographicsModel.java index 83450844..044650ea 100644 --- a/src/main/java/com/iemr/common/model/beneficiary/BeneficiaryDemographicsModel.java +++ b/src/main/java/com/iemr/common/model/beneficiary/BeneficiaryDemographicsModel.java @@ -173,6 +173,19 @@ public class BeneficiaryDemographicsModel { @Expose private String habitation; + @Expose + private Double latitude; + @Expose + private Double longitude; + @Expose + private String digipin; + @Expose + private Long gpsTimestamp; + @Expose + private Boolean isGpsUnavailable = false; + @Expose + private String gpsUnavailableReason; + @Expose private String occupation; @Expose diff --git a/src/main/java/com/iemr/common/service/health/HealthService.java b/src/main/java/com/iemr/common/service/health/HealthService.java index 7714efce..0c905d0a 100644 --- a/src/main/java/com/iemr/common/service/health/HealthService.java +++ b/src/main/java/com/iemr/common/service/health/HealthService.java @@ -72,7 +72,6 @@ public class HealthService { 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 @@ -183,9 +182,11 @@ private Map checkDatabaseConnectivity() { stmt.setQueryTimeout(3); // Bounds only the SELECT 1 execution stmt.execute("SELECT 1"); - // If SELECT 1 succeeds, use cached severity from background diagnostics + // SELECT 1 succeeded, so connectivity is UP regardless of background severity. + // Severity is a separate capacity/performance signal (pool usage, long transactions, + // etc.) — it must not be reported as a connectivity outage. String severity = cachedDbSeverity.get(); - result.put(FIELD_STATUS, resolveDatabaseStatus(severity)); + result.put(FIELD_STATUS, STATUS_UP); result.put(FIELD_SEVERITY, severity); } catch (Exception e) { @@ -402,13 +403,6 @@ private String performConnectionUsageCheck(Connection conn) { } return SEVERITY_OK; } - private String resolveDatabaseStatus(String severity) { - return switch (severity) { - case SEVERITY_CRITICAL -> STATUS_DOWN; - case SEVERITY_WARNING -> STATUS_DEGRADED; - default -> STATUS_UP; - }; - } private String escalate(String current, String candidate) { return severityRank(candidate) > severityRank(current) ? candidate : current; } From d5c079c24f5c8f63beca1a636db30605cd754679 Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Thu, 23 Jul 2026 16:23:01 +0530 Subject: [PATCH 19/32] save fcm user token in table --- .../common/data/userToken/UserTokenData.java | 17 +++++++++-------- .../model/notification/NotificationMessage.java | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/iemr/common/data/userToken/UserTokenData.java b/src/main/java/com/iemr/common/data/userToken/UserTokenData.java index 0646972e..9a6da29d 100644 --- a/src/main/java/com/iemr/common/data/userToken/UserTokenData.java +++ b/src/main/java/com/iemr/common/data/userToken/UserTokenData.java @@ -24,23 +24,24 @@ */ package com.iemr.common.data.userToken; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.Table; +import jakarta.persistence.*; import lombok.Data; import java.sql.Timestamp; +import static jakarta.persistence.GenerationType.IDENTITY; + @Entity -@Table(name = "user_tokens", schema = "db_iemr") +@Table(name = "user_fcm_tokens", schema = "db_iemr") @Data public class UserTokenData { @Id + @GeneratedValue(strategy = IDENTITY) + private int id; @Column(name = "user_id") - Integer userId; + private Integer userId; @Column(name = "token") - String token; + private String token; @Column(name = "updated_at") - Timestamp updatedAt; + private Timestamp updatedAt; } diff --git a/src/main/java/com/iemr/common/model/notification/NotificationMessage.java b/src/main/java/com/iemr/common/model/notification/NotificationMessage.java index 58343034..55cb0e62 100644 --- a/src/main/java/com/iemr/common/model/notification/NotificationMessage.java +++ b/src/main/java/com/iemr/common/model/notification/NotificationMessage.java @@ -35,4 +35,4 @@ public class NotificationMessage { private String title; private String body; private Map data; -} +} \ No newline at end of file From e9b38a27ae04c030c644d2f4524b829ea22eef13 Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Thu, 23 Jul 2026 16:47:02 +0530 Subject: [PATCH 20/32] save fcm user token in table --- .../java/com/iemr/common/repo/userToken/UserTokenRepo.java | 3 +++ .../firebaseNotification/FirebaseNotificationService.java | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/iemr/common/repo/userToken/UserTokenRepo.java b/src/main/java/com/iemr/common/repo/userToken/UserTokenRepo.java index 817f26f0..10e504ae 100644 --- a/src/main/java/com/iemr/common/repo/userToken/UserTokenRepo.java +++ b/src/main/java/com/iemr/common/repo/userToken/UserTokenRepo.java @@ -26,6 +26,9 @@ import com.iemr.common.data.userToken.UserTokenData; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository public interface UserTokenRepo extends JpaRepository { } diff --git a/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java b/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java index 7f33c453..01de9df3 100644 --- a/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java +++ b/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java @@ -94,12 +94,10 @@ public String updateToken(UserToken userToken) { if (existingTokenData.isPresent()) { userTokenData = existingTokenData.get(); userTokenData.setToken(userToken.getToken()); - userTokenData.setUpdatedAt(new Timestamp(System.currentTimeMillis())); } else { userTokenData = new UserTokenData(); userTokenData.setUserId(userToken.getUserId()); userTokenData.setToken(userToken.getToken()); - userTokenData.setUpdatedAt(new Timestamp(System.currentTimeMillis())); } userTokenRepo.save(userTokenData); From 3714b6d51436453305aa29532e0e05ce2aba5ebb Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Thu, 23 Jul 2026 16:53:15 +0530 Subject: [PATCH 21/32] save fcm user token in table --- .../{UserTokenData.java => UserFcmTokenData.java} | 2 +- .../{UserTokenRepo.java => UserFcmTokenRepo.java} | 4 ++-- .../FirebaseNotificationService.java | 15 +++++++-------- 3 files changed, 10 insertions(+), 11 deletions(-) rename src/main/java/com/iemr/common/data/userToken/{UserTokenData.java => UserFcmTokenData.java} (97%) rename src/main/java/com/iemr/common/repo/userToken/{UserTokenRepo.java => UserFcmTokenRepo.java} (88%) diff --git a/src/main/java/com/iemr/common/data/userToken/UserTokenData.java b/src/main/java/com/iemr/common/data/userToken/UserFcmTokenData.java similarity index 97% rename from src/main/java/com/iemr/common/data/userToken/UserTokenData.java rename to src/main/java/com/iemr/common/data/userToken/UserFcmTokenData.java index 9a6da29d..0a0fefbe 100644 --- a/src/main/java/com/iemr/common/data/userToken/UserTokenData.java +++ b/src/main/java/com/iemr/common/data/userToken/UserFcmTokenData.java @@ -34,7 +34,7 @@ @Entity @Table(name = "user_fcm_tokens", schema = "db_iemr") @Data -public class UserTokenData { +public class UserFcmTokenData { @Id @GeneratedValue(strategy = IDENTITY) private int id; diff --git a/src/main/java/com/iemr/common/repo/userToken/UserTokenRepo.java b/src/main/java/com/iemr/common/repo/userToken/UserFcmTokenRepo.java similarity index 88% rename from src/main/java/com/iemr/common/repo/userToken/UserTokenRepo.java rename to src/main/java/com/iemr/common/repo/userToken/UserFcmTokenRepo.java index 10e504ae..ccc1d6d9 100644 --- a/src/main/java/com/iemr/common/repo/userToken/UserTokenRepo.java +++ b/src/main/java/com/iemr/common/repo/userToken/UserFcmTokenRepo.java @@ -24,11 +24,11 @@ */ package com.iemr.common.repo.userToken; -import com.iemr.common.data.userToken.UserTokenData; +import com.iemr.common.data.userToken.UserFcmTokenData; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository -public interface UserTokenRepo extends JpaRepository { +public interface UserFcmTokenRepo extends JpaRepository { } diff --git a/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java b/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java index 01de9df3..9514620f 100644 --- a/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java +++ b/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java @@ -28,10 +28,10 @@ import com.google.firebase.messaging.FirebaseMessaging; import com.google.firebase.messaging.Message; import com.google.firebase.messaging.Notification; -import com.iemr.common.data.userToken.UserTokenData; +import com.iemr.common.data.userToken.UserFcmTokenData; import com.iemr.common.model.notification.NotificationMessage; import com.iemr.common.model.notification.UserToken; -import com.iemr.common.repo.userToken.UserTokenRepo; +import com.iemr.common.repo.userToken.UserFcmTokenRepo; import com.iemr.common.utils.CookieUtil; import com.iemr.common.utils.JwtUtil; import com.iemr.common.utils.exception.IEMRException; @@ -43,7 +43,6 @@ import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; -import java.sql.Timestamp; import java.util.Optional; @Service @@ -54,7 +53,7 @@ public class FirebaseNotificationService { FirebaseMessaging firebaseMessaging; @Autowired - private UserTokenRepo userTokenRepo; + private UserFcmTokenRepo userTokenRepo; @Autowired private CookieUtil cookieUtil; @@ -87,15 +86,15 @@ public String sendNotification(NotificationMessage notificationMessage) { } public String updateToken(UserToken userToken) { - Optional existingTokenData = userTokenRepo.findById(userToken.getUserId()); + Optional existingTokenData = userTokenRepo.findById(userToken.getUserId()); - UserTokenData userTokenData; + UserFcmTokenData userTokenData; if (existingTokenData.isPresent()) { userTokenData = existingTokenData.get(); userTokenData.setToken(userToken.getToken()); } else { - userTokenData = new UserTokenData(); + userTokenData = new UserFcmTokenData(); userTokenData.setUserId(userToken.getUserId()); userTokenData.setToken(userToken.getToken()); } @@ -109,7 +108,7 @@ public String getUserToken() throws IEMRException { .getRequest(); String jwtTokenFromCookie = cookieUtil.getJwtTokenFromCookie(requestHeader); return userTokenRepo.findById(Integer.parseInt(jwtUtil.getUserIdFromToken(jwtTokenFromCookie))) // because your userId is Long in DB - .map(UserTokenData::getToken) + .map(UserFcmTokenData::getToken) .orElse(null); // } From 251cf6fa361d34032ea35b97b22b47cfb8ab2d66 Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Thu, 23 Jul 2026 17:01:57 +0530 Subject: [PATCH 22/32] save fcm user token in table --- .../firebaseNotification/FirebaseNotificationService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java b/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java index 9514620f..7dbee27f 100644 --- a/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java +++ b/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java @@ -72,7 +72,7 @@ public String sendNotification(NotificationMessage notificationMessage) { Notification notification = Notification.builder().setTitle(notificationMessage.getTitle()).setBody(notificationMessage.getBody()).build(); - Message message = Message.builder().setTopic(notificationMessage.getToken()).setNotification(notification).putAllData(notificationMessage.getData()).build(); + Message message = Message.builder().setToken(notificationMessage.getToken()).setNotification(notification).putAllData(notificationMessage.getData()).build(); try { From 4ef61c244c4c01aa67e8ddd18e33048962d61195 Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Thu, 23 Jul 2026 17:38:38 +0530 Subject: [PATCH 23/32] save fcm user token in table --- .../FirebaseNotificationController.java | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/iemr/common/controller/firebaseNotification/FirebaseNotificationController.java b/src/main/java/com/iemr/common/controller/firebaseNotification/FirebaseNotificationController.java index 3bb10cfc..e37f2c41 100644 --- a/src/main/java/com/iemr/common/controller/firebaseNotification/FirebaseNotificationController.java +++ b/src/main/java/com/iemr/common/controller/firebaseNotification/FirebaseNotificationController.java @@ -28,9 +28,12 @@ import com.iemr.common.model.notification.UserToken; import com.iemr.common.service.firebaseNotification.FirebaseNotificationService; import com.iemr.common.utils.exception.IEMRException; +import com.iemr.common.utils.response.ApiResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @RestController @@ -41,17 +44,33 @@ public class FirebaseNotificationController { @Autowired FirebaseNotificationService firebaseNotificationService; - @RequestMapping(value = "sendNotification",method = RequestMethod.POST,headers = "Authorization") + @PostMapping(value = "/sendNotification") public String sendNotificationByToken(@RequestBody NotificationMessage notificationMessage){ return firebaseNotificationService.sendNotification(notificationMessage); } - @RequestMapping(value = "updateToken",method = RequestMethod.POST,headers = "Authorization") - public String updateToken(@RequestBody UserToken userToken){ - return firebaseNotificationService.updateToken(userToken); + @PostMapping("/updateToken") + public ResponseEntity updateToken(@RequestBody UserToken userToken) { + try { + + Object result = firebaseNotificationService.updateToken(userToken); + + return ResponseEntity.ok(result); + + } catch (IllegalArgumentException e) { + + return ResponseEntity.badRequest().body(e.getMessage()); + + } catch (Exception e) { + + logger.error("Error while updating Firebase token", e); + + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Failed to update Firebase token."); + } } - @RequestMapping(value = "getToken",method = RequestMethod.GET,headers = "Authorization") + @PostMapping(value = "getToken") public String getUserToken() throws IEMRException { return firebaseNotificationService.getUserToken(); From 65c4f93d52cdbb9de8815a1e45d4c46822270cfa Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Thu, 23 Jul 2026 17:40:57 +0530 Subject: [PATCH 24/32] save fcm user token in table --- .../java/com/iemr/common/repo/userToken/UserFcmTokenRepo.java | 4 ++++ .../firebaseNotification/FirebaseNotificationService.java | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/iemr/common/repo/userToken/UserFcmTokenRepo.java b/src/main/java/com/iemr/common/repo/userToken/UserFcmTokenRepo.java index ccc1d6d9..b2ed14df 100644 --- a/src/main/java/com/iemr/common/repo/userToken/UserFcmTokenRepo.java +++ b/src/main/java/com/iemr/common/repo/userToken/UserFcmTokenRepo.java @@ -28,7 +28,11 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.Optional; + @Repository public interface UserFcmTokenRepo extends JpaRepository { + + Optional findByUserId(Integer userId); } diff --git a/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java b/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java index 7dbee27f..52591cb4 100644 --- a/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java +++ b/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java @@ -86,7 +86,7 @@ public String sendNotification(NotificationMessage notificationMessage) { } public String updateToken(UserToken userToken) { - Optional existingTokenData = userTokenRepo.findById(userToken.getUserId()); + Optional existingTokenData = userTokenRepo.findByUserId(userToken.getUserId()); UserFcmTokenData userTokenData; From ac356240af02e1d66b79008de48b0221464146f4 Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Fri, 24 Jul 2026 12:35:13 +0530 Subject: [PATCH 25/32] save fcm user token in table --- .../FirebaseNotificationController.java | 33 +++++++++++++++-- .../FirebaseNotificationService.java | 35 +++++++++++++++---- 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/iemr/common/controller/firebaseNotification/FirebaseNotificationController.java b/src/main/java/com/iemr/common/controller/firebaseNotification/FirebaseNotificationController.java index e37f2c41..1cb2753d 100644 --- a/src/main/java/com/iemr/common/controller/firebaseNotification/FirebaseNotificationController.java +++ b/src/main/java/com/iemr/common/controller/firebaseNotification/FirebaseNotificationController.java @@ -44,9 +44,36 @@ public class FirebaseNotificationController { @Autowired FirebaseNotificationService firebaseNotificationService; - @PostMapping(value = "/sendNotification") - public String sendNotificationByToken(@RequestBody NotificationMessage notificationMessage){ - return firebaseNotificationService.sendNotification(notificationMessage); + + @PostMapping("/sendNotification") + public ResponseEntity sendNotificationByToken(@RequestBody NotificationMessage notificationMessage) { + + logger.info("Received notification request. Token={}, Title={}, ReceiverId={}, Type={}", + notificationMessage.getToken(), + notificationMessage.getTitle(), + notificationMessage.getData() != null ? notificationMessage.getData().get("receiver_user_id") : null, + notificationMessage.getData() != null ? notificationMessage.getData().get("notification_type") : null); + + logger.debug("Notification payload: {}", notificationMessage); + + try { + String response = firebaseNotificationService.sendNotification(notificationMessage); + + logger.info("Notification processed successfully. Response={}", response); + + return ResponseEntity.ok(response); + + } catch (Exception e) { + + logger.error("Failed to process notification request. Token={}, ReceiverId={}, Error={}", + notificationMessage.getToken(), + notificationMessage.getData() != null ? notificationMessage.getData().get("receiver_user_id") : null, + e.getMessage(), + e); + + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Failed to send notification"); + } } @PostMapping("/updateToken") diff --git a/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java b/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java index 52591cb4..834ce443 100644 --- a/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java +++ b/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java @@ -26,8 +26,10 @@ import com.google.firebase.FirebaseException; import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.FirebaseMessagingException; import com.google.firebase.messaging.Message; import com.google.firebase.messaging.Notification; +import com.google.gson.Gson; import com.iemr.common.data.userToken.UserFcmTokenData; import com.iemr.common.model.notification.NotificationMessage; import com.iemr.common.model.notification.UserToken; @@ -65,23 +67,42 @@ public class FirebaseNotificationService { public String sendNotification(NotificationMessage notificationMessage) { + + logger.info("Notification Request : {}", new Gson().toJson(notificationMessage)); + if (firebaseMessaging == null) { - logger.error("⚠️ Firebase is not configured, skipping notification"); - return null; + logger.error("Firebase is not configured"); + return "Firebase is not configured"; } - Notification notification = Notification.builder().setTitle(notificationMessage.getTitle()).setBody(notificationMessage.getBody()).build(); + try { - Message message = Message.builder().setToken(notificationMessage.getToken()).setNotification(notification).putAllData(notificationMessage.getData()).build(); + Notification notification = Notification.builder() + .setTitle(notificationMessage.getTitle()) + .setBody(notificationMessage.getBody()) + .build(); + Message message = Message.builder() + .setToken(notificationMessage.getToken()) + .setNotification(notification) + .putAllData(notificationMessage.getData()) + .build(); - try { String response = FirebaseMessaging.getInstance().send(message); + logger.info("FCM Success : {}", response); + return response; - } catch (FirebaseException e) { - return "Error sending notification"; + } catch (FirebaseMessagingException e) { + + logger.error("FirebaseMessagingException : {}", e.getMessage(), e); + return e.getMessage(); + + } catch (Exception e) { + + logger.error("Unexpected Exception : {}", e.getMessage(), e); + throw e; } } From 4f58051954978a7bdce865ee338ccb2fa0b28b80 Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Fri, 24 Jul 2026 13:55:32 +0530 Subject: [PATCH 26/32] save fcm user token in table --- .../firebaseNotification/FirebaseNotificationService.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java b/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java index 834ce443..809eda19 100644 --- a/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java +++ b/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java @@ -30,6 +30,7 @@ import com.google.firebase.messaging.Message; import com.google.firebase.messaging.Notification; import com.google.gson.Gson; +import com.iemr.common.config.firebase.FirebaseMessagingConfig; import com.iemr.common.data.userToken.UserFcmTokenData; import com.iemr.common.model.notification.NotificationMessage; import com.iemr.common.model.notification.UserToken; @@ -51,8 +52,8 @@ public class FirebaseNotificationService { final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); - @Autowired(required = false) - FirebaseMessaging firebaseMessaging; + @Autowired + FirebaseMessagingConfig firebaseMessaging; @Autowired private UserFcmTokenRepo userTokenRepo; From 7b4b14fde2cac43d761eb9b6a92a9275c714c7d5 Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Fri, 24 Jul 2026 13:58:02 +0530 Subject: [PATCH 27/32] save fcm user token in table --- .../iemr/common/config/firebase/FirebaseMessagingConfig.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/iemr/common/config/firebase/FirebaseMessagingConfig.java b/src/main/java/com/iemr/common/config/firebase/FirebaseMessagingConfig.java index ebb697ec..baca2bc2 100644 --- a/src/main/java/com/iemr/common/config/firebase/FirebaseMessagingConfig.java +++ b/src/main/java/com/iemr/common/config/firebase/FirebaseMessagingConfig.java @@ -21,10 +21,10 @@ public class FirebaseMessagingConfig { private Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName()); - @Value("${firebase.enabled:false}") + @Value("${firebase.enabled}") private boolean firebaseEnabled; - @Value("${firebase.credential-file:}") + @Value("${firebase.credential-file}") private String firebaseCredentialFile; From 865ede141dee758c3b9091277281cee611a91b5c Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Fri, 24 Jul 2026 14:12:34 +0530 Subject: [PATCH 28/32] save fcm user token in table --- .../FirebaseNotificationService.java | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java b/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java index 809eda19..7d642c74 100644 --- a/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java +++ b/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java @@ -53,7 +53,10 @@ public class FirebaseNotificationService { final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); @Autowired - FirebaseMessagingConfig firebaseMessaging; + FirebaseMessagingConfig firebaseMessagingConfig; + + @Autowired(required = false) + private FirebaseMessaging firebaseMessaging; @Autowired private UserFcmTokenRepo userTokenRepo; @@ -69,11 +72,12 @@ public class FirebaseNotificationService { public String sendNotification(NotificationMessage notificationMessage) { - logger.info("Notification Request : {}", new Gson().toJson(notificationMessage)); + logger.info("========== FCM Notification Request =========="); + logger.info("Request : {}", new Gson().toJson(notificationMessage)); if (firebaseMessaging == null) { - logger.error("Firebase is not configured"); - return "Firebase is not configured"; + logger.error("FirebaseMessaging bean is not initialized."); + return "FirebaseMessaging bean is not initialized."; } try { @@ -89,24 +93,30 @@ public String sendNotification(NotificationMessage notificationMessage) { .putAllData(notificationMessage.getData()) .build(); - String response = FirebaseMessaging.getInstance().send(message); + logger.info("Sending notification to token: {}", notificationMessage.getToken()); + + String response = firebaseMessaging.send(message); - logger.info("FCM Success : {}", response); + logger.info("Notification sent successfully."); + logger.info("Firebase Message Id : {}", response); return response; } catch (FirebaseMessagingException e) { - logger.error("FirebaseMessagingException : {}", e.getMessage(), e); - return e.getMessage(); + logger.error("FirebaseMessagingException"); + logger.error("Error Code : {}", e.getMessagingErrorCode()); + logger.error("Error Message : {}", e.getMessage(), e); + + return "FCM Error : " + e.getMessage(); } catch (Exception e) { - logger.error("Unexpected Exception : {}", e.getMessage(), e); - throw e; + logger.error("Unexpected exception while sending notification.", e); + + return "Unexpected Error : " + e.getMessage(); } } - public String updateToken(UserToken userToken) { Optional existingTokenData = userTokenRepo.findByUserId(userToken.getUserId()); From c4cfb5d1907b608c11db4086fb0f6cd05b1b09cf Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Fri, 24 Jul 2026 14:25:22 +0530 Subject: [PATCH 29/32] save fcm user token in table --- .../iemr/common/config/firebase/FirebaseMessagingConfig.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/iemr/common/config/firebase/FirebaseMessagingConfig.java b/src/main/java/com/iemr/common/config/firebase/FirebaseMessagingConfig.java index baca2bc2..7aae4c89 100644 --- a/src/main/java/com/iemr/common/config/firebase/FirebaseMessagingConfig.java +++ b/src/main/java/com/iemr/common/config/firebase/FirebaseMessagingConfig.java @@ -31,6 +31,9 @@ public class FirebaseMessagingConfig { @Bean @ConditionalOnProperty(name = "firebase.enabled", havingValue = "true") public FirebaseMessaging firebaseMessaging() throws IOException { + logger.info("===== Initializing Firebase ====="); + logger.info("firebaseEnabled={}", firebaseEnabled); + logger.info("firebaseCredentialFile={}", firebaseCredentialFile); if (!firebaseEnabled) { logger.error("⚠️ Firebase disabled by config"); return null; From 3e31edae6f0b99de5170cf570c26d14e01a5aa48 Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Fri, 24 Jul 2026 15:30:42 +0530 Subject: [PATCH 30/32] save fcm user token in table --- .../FirebaseNotificationService.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java b/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java index 7d642c74..691318db 100644 --- a/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java +++ b/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java @@ -42,6 +42,7 @@ 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.stereotype.Service; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; @@ -69,9 +70,20 @@ public class FirebaseNotificationService { private Message message; + @Value("${firebase.enabled}") + private boolean firebaseEnabled; + + @Value("${firebase.credential-file}") + private String firebaseCredentialFile; + + public String sendNotification(NotificationMessage notificationMessage) { + logger.info("===== Initializing Firebase ====="); + logger.info("firebaseEnabled={}", firebaseEnabled); + logger.info("firebaseCredentialFile={}", firebaseCredentialFile); + logger.info("========== FCM Notification Request =========="); logger.info("Request : {}", new Gson().toJson(notificationMessage)); From 700e2e2010613a3391308668e4729827f0b1da5d Mon Sep 17 00:00:00 2001 From: Saurav Mishra Date: Fri, 24 Jul 2026 15:41:54 +0530 Subject: [PATCH 31/32] save fcm user token in table --- .../common/config/firebase/FirebaseMessagingConfig.java | 8 +++++--- .../firebaseNotification/FirebaseNotificationService.java | 3 --- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/iemr/common/config/firebase/FirebaseMessagingConfig.java b/src/main/java/com/iemr/common/config/firebase/FirebaseMessagingConfig.java index 7aae4c89..08d9a518 100644 --- a/src/main/java/com/iemr/common/config/firebase/FirebaseMessagingConfig.java +++ b/src/main/java/com/iemr/common/config/firebase/FirebaseMessagingConfig.java @@ -45,9 +45,11 @@ public FirebaseMessaging firebaseMessaging() throws IOException { return null; // don't throw, app will still start } - GoogleCredentials credentials = GoogleCredentials.fromStream( - new ClassPathResource(firebaseCredentialFile).getInputStream() - ); + GoogleCredentials credentials = + GoogleCredentials.fromStream( + new FileInputStream(firebaseCredentialFile) + ); + FirebaseOptions options = FirebaseOptions.builder() .setCredentials(credentials) .build(); diff --git a/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java b/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java index 691318db..66140ccc 100644 --- a/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java +++ b/src/main/java/com/iemr/common/service/firebaseNotification/FirebaseNotificationService.java @@ -53,9 +53,6 @@ public class FirebaseNotificationService { final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); - @Autowired - FirebaseMessagingConfig firebaseMessagingConfig; - @Autowired(required = false) private FirebaseMessaging firebaseMessaging; From 27662f6883bb4d5958a368ff4005df5fbe0d7bb6 Mon Sep 17 00:00:00 2001 From: Vishwanath Balkur <118195001+vishwab1@users.noreply.github.com> Date: Fri, 31 Jul 2026 11:36:09 +0530 Subject: [PATCH 32/32] fix(stoptb): resolve district/block/village against Nikshay masters, not standard AMRIT masters (#444) Stop TB workers are mapped to Nikshay's own isolated location hierarchy (m_nikshay_district/tu/village), a separate ID space from AMRIT's standard masters (m_district/m_districtblock/m_DistrictBranchMapping). The beneficiary address mapper ignored the districtName/districtBranchName strings sent by mobile and re-derived them by looking the numeric ID up in the standard masters instead, so every Stop TB registration got stamped with whatever unrelated place happened to share that ID (e.g. district 1 -> "Nicobars" instead of "Alluri Sitharama Raju"). Add Nikshay-specific lookup entities/repos and a NikshayAddressResolver that gates on providerServiceMapID -> ServiceName == "Stop TB". Only Stop TB beneficiaries take the Nikshay resolution path; every other service line's address mapping is untouched. Hibernate ddl-auto=none means tenants without the Nikshay tables are unaffected at both startup and runtime, since the new repositories are never queried unless isStopTB is true. Co-authored-by: Claude Sonnet 5 --- .../common/data/location/NikshayDistrict.java | 44 ++++++++++ .../iemr/common/data/location/NikshayTU.java | 44 ++++++++++ .../common/data/location/NikshayVillage.java | 44 ++++++++++ .../mapper/CommonIdentityMapperDecorator.java | 37 ++++++--- .../IdentityBenEditMapperDecorator.java | 35 ++++++-- .../location/NikshayDistrictRepository.java | 31 +++++++ .../location/NikshayTURepository.java | 31 +++++++ .../location/NikshayVillageRepository.java | 31 +++++++ .../location/NikshayAddressResolver.java | 82 +++++++++++++++++++ 9 files changed, 359 insertions(+), 20 deletions(-) create mode 100644 src/main/java/com/iemr/common/data/location/NikshayDistrict.java create mode 100644 src/main/java/com/iemr/common/data/location/NikshayTU.java create mode 100644 src/main/java/com/iemr/common/data/location/NikshayVillage.java create mode 100644 src/main/java/com/iemr/common/repository/location/NikshayDistrictRepository.java create mode 100644 src/main/java/com/iemr/common/repository/location/NikshayTURepository.java create mode 100644 src/main/java/com/iemr/common/repository/location/NikshayVillageRepository.java create mode 100644 src/main/java/com/iemr/common/service/location/NikshayAddressResolver.java diff --git a/src/main/java/com/iemr/common/data/location/NikshayDistrict.java b/src/main/java/com/iemr/common/data/location/NikshayDistrict.java new file mode 100644 index 00000000..6c9771aa --- /dev/null +++ b/src/main/java/com/iemr/common/data/location/NikshayDistrict.java @@ -0,0 +1,44 @@ +/* +* 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.common.data.location; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Data; + +/** + * Nikshay's own district master (m_nikshay_district) — an isolated ID space + * from AMRIT's standard m_district, used only for Stop TB location mapping. + */ +@Entity +@Table(name = "m_nikshay_district") +@Data +public class NikshayDistrict { + @Id + @Column(name = "NikshayDistrictID") + private Integer nikshayDistrictID; + + @Column(name = "DistrictName") + private String districtName; +} diff --git a/src/main/java/com/iemr/common/data/location/NikshayTU.java b/src/main/java/com/iemr/common/data/location/NikshayTU.java new file mode 100644 index 00000000..09071ff0 --- /dev/null +++ b/src/main/java/com/iemr/common/data/location/NikshayTU.java @@ -0,0 +1,44 @@ +/* +* 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.common.data.location; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Data; + +/** + * Nikshay's Tuberculosis Unit master (m_nikshay_tu) — Stop TB's equivalent + * of a "block", but scoped to Nikshay's own hierarchy, not m_districtblock. + */ +@Entity +@Table(name = "m_nikshay_tu") +@Data +public class NikshayTU { + @Id + @Column(name = "NikshayTUID") + private Integer nikshayTUID; + + @Column(name = "TUName") + private String tuName; +} diff --git a/src/main/java/com/iemr/common/data/location/NikshayVillage.java b/src/main/java/com/iemr/common/data/location/NikshayVillage.java new file mode 100644 index 00000000..d89825f8 --- /dev/null +++ b/src/main/java/com/iemr/common/data/location/NikshayVillage.java @@ -0,0 +1,44 @@ +/* +* 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.common.data.location; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Data; + +/** + * Nikshay's own village master (m_nikshay_village) — an isolated ID space + * from AMRIT's standard m_DistrictBranchMapping, used only for Stop TB. + */ +@Entity +@Table(name = "m_nikshay_village") +@Data +public class NikshayVillage { + @Id + @Column(name = "NikshayVillageID") + private Integer nikshayVillageID; + + @Column(name = "VillageName") + private String villageName; +} diff --git a/src/main/java/com/iemr/common/mapper/CommonIdentityMapperDecorator.java b/src/main/java/com/iemr/common/mapper/CommonIdentityMapperDecorator.java index f18920ad..3ee5acff 100644 --- a/src/main/java/com/iemr/common/mapper/CommonIdentityMapperDecorator.java +++ b/src/main/java/com/iemr/common/mapper/CommonIdentityMapperDecorator.java @@ -29,8 +29,11 @@ import com.iemr.common.dto.identity.Identity; import com.iemr.common.model.beneficiary.BeneficiaryDemographicsModel; import com.iemr.common.model.beneficiary.BeneficiaryModel; +import com.iemr.common.service.location.NikshayAddressResolver; public abstract class CommonIdentityMapperDecorator implements CommonIdentityMapper { + @Autowired + NikshayAddressResolver nikshayAddressResolver; @Autowired StateMapper stateMapper; @Autowired @@ -69,11 +72,13 @@ public CommonIdentityDTO beneficiaryModelCommonIdentityDTO(BeneficiaryModel bene return null; } CommonIdentityDTO commonIdentityDTO = new CommonIdentityDTO(); - commonIdentityDTO - .setPermanentAddress(beneficiaryDemographicsModelToAddress(beneficiary.getI_bendemographics())); - commonIdentityDTO.setCurrentAddress(beneficiaryDemographicsModelToAddress(beneficiary.getI_bendemographics())); - commonIdentityDTO - .setEmergencyAddress(beneficiaryDemographicsModelToAddress(beneficiary.getI_bendemographics())); + boolean isStopTB = nikshayAddressResolver.isStopTB(beneficiary.getProviderServiceMapID()); + commonIdentityDTO.setPermanentAddress( + beneficiaryDemographicsModelToAddress(beneficiary.getI_bendemographics(), isStopTB)); + commonIdentityDTO.setCurrentAddress( + beneficiaryDemographicsModelToAddress(beneficiary.getI_bendemographics(), isStopTB)); + commonIdentityDTO.setEmergencyAddress( + beneficiaryDemographicsModelToAddress(beneficiary.getI_bendemographics(), isStopTB)); commonIdentityDTO.setPlaceOfWork(beneficiary.getPlaceOfWork()); commonIdentityDTO.setMarriageDate(beneficiary.getMarriageDate()); commonIdentityDTO.setIsHIVPositive(beneficiary.getIsHIVPos()); @@ -210,6 +215,11 @@ public CommonIdentityDTO beneficiaryModelCommonIdentityDTO(BeneficiaryModel bene } protected Address beneficiaryDemographicsModelToAddress(BeneficiaryDemographicsModel beneficiaryDemographicsModel) { + return beneficiaryDemographicsModelToAddress(beneficiaryDemographicsModel, false); + } + + protected Address beneficiaryDemographicsModelToAddress(BeneficiaryDemographicsModel beneficiaryDemographicsModel, + boolean isStopTB) { if (beneficiaryDemographicsModel == null) { return null; } @@ -222,8 +232,10 @@ protected Address beneficiaryDemographicsModelToAddress(BeneficiaryDemographicsM address.setStateId(beneficiaryDemographicsModel.getStateID()); address.setDistrictId(beneficiaryDemographicsModel.getDistrictID()); if (beneficiaryDemographicsModel.getDistrictID() != null) { - address.setDistrict( - districtMapper.districtToModelByID(beneficiaryDemographicsModel.getDistrictID()).getDistrictName()); + address.setDistrict(isStopTB + ? nikshayAddressResolver.resolveDistrictName(beneficiaryDemographicsModel.getDistrictID()) + : districtMapper.districtToModelByID(beneficiaryDemographicsModel.getDistrictID()) + .getDistrictName()); } address.setServicePointName(beneficiaryDemographicsModel.getServicePointName()); address.setCountry(beneficiaryDemographicsModel.getCountryName()); @@ -239,14 +251,17 @@ protected Address beneficiaryDemographicsModelToAddress(BeneficiaryDemographicsM address.setParkingPlaceID(beneficiaryDemographicsModel.getParkingPlaceID()); address.setServicePointID(beneficiaryDemographicsModel.getServicePointID()); if (beneficiaryDemographicsModel.getDistrictBranchID() != null) { - address.setVillage(branchMapper - .districtBranchToModelByID(beneficiaryDemographicsModel.getDistrictBranchID()).getVillageName()); + address.setVillage(isStopTB + ? nikshayAddressResolver.resolveVillageName(beneficiaryDemographicsModel.getDistrictBranchID()) + : branchMapper.districtBranchToModelByID(beneficiaryDemographicsModel.getDistrictBranchID()) + .getVillageName()); } address.setSubDistrictId(beneficiaryDemographicsModel.getBlockID()); address.setCountryId(beneficiaryDemographicsModel.getCountryID()); if (beneficiaryDemographicsModel.getBlockID() != null) { - address.setSubDistrict( - blockMapper.districtBlockToModelByID(beneficiaryDemographicsModel.getBlockID()).getBlockName()); + address.setSubDistrict(isStopTB + ? nikshayAddressResolver.resolveTUName(beneficiaryDemographicsModel.getBlockID()) + : blockMapper.districtBlockToModelByID(beneficiaryDemographicsModel.getBlockID()).getBlockName()); } address.setGpsLatitude(beneficiaryDemographicsModel.getLatitude()); address.setGpsLongitude(beneficiaryDemographicsModel.getLongitude()); diff --git a/src/main/java/com/iemr/common/mapper/IdentityBenEditMapperDecorator.java b/src/main/java/com/iemr/common/mapper/IdentityBenEditMapperDecorator.java index 27b4a602..999ddf67 100644 --- a/src/main/java/com/iemr/common/mapper/IdentityBenEditMapperDecorator.java +++ b/src/main/java/com/iemr/common/mapper/IdentityBenEditMapperDecorator.java @@ -33,8 +33,11 @@ import com.iemr.common.model.beneficiary.BenPhoneMapModel; import com.iemr.common.model.beneficiary.BeneficiaryDemographicsModel; import com.iemr.common.model.beneficiary.BeneficiaryModel; +import com.iemr.common.service.location.NikshayAddressResolver; public abstract class IdentityBenEditMapperDecorator implements IdentityBenEditMapper { + @Autowired + NikshayAddressResolver nikshayAddressResolver; @Autowired StateMapper stateMapper; @Autowired @@ -69,6 +72,11 @@ public abstract class IdentityBenEditMapperDecorator implements IdentityBenEditM MaritalStatusMapper maritalStatusMapper; protected Address beneficiaryDemographicsModelToAddress(BeneficiaryDemographicsModel beneficiaryDemographicsModel) { + return beneficiaryDemographicsModelToAddress(beneficiaryDemographicsModel, false); + } + + protected Address beneficiaryDemographicsModelToAddress(BeneficiaryDemographicsModel beneficiaryDemographicsModel, + boolean isStopTB) { if (beneficiaryDemographicsModel == null) { return null; } @@ -81,8 +89,10 @@ protected Address beneficiaryDemographicsModelToAddress(BeneficiaryDemographicsM address.setStateId(beneficiaryDemographicsModel.getStateID()); address.setDistrictId(beneficiaryDemographicsModel.getDistrictID()); if (beneficiaryDemographicsModel.getDistrictID() != null) { - address.setDistrict( - districtMapper.districtToModelByID(beneficiaryDemographicsModel.getDistrictID()).getDistrictName()); + address.setDistrict(isStopTB + ? nikshayAddressResolver.resolveDistrictName(beneficiaryDemographicsModel.getDistrictID()) + : districtMapper.districtToModelByID(beneficiaryDemographicsModel.getDistrictID()) + .getDistrictName()); } address.setServicePointName(beneficiaryDemographicsModel.getServicePointName()); address.setCountry(beneficiaryDemographicsModel.getCountryName()); @@ -98,14 +108,17 @@ protected Address beneficiaryDemographicsModelToAddress(BeneficiaryDemographicsM address.setParkingPlaceID(beneficiaryDemographicsModel.getParkingPlaceID()); address.setServicePointID(beneficiaryDemographicsModel.getServicePointID()); if (beneficiaryDemographicsModel.getDistrictBranchID() != null) { - address.setVillage(branchMapper - .districtBranchToModelByID(beneficiaryDemographicsModel.getDistrictBranchID()).getVillageName()); + address.setVillage(isStopTB + ? nikshayAddressResolver.resolveVillageName(beneficiaryDemographicsModel.getDistrictBranchID()) + : branchMapper.districtBranchToModelByID(beneficiaryDemographicsModel.getDistrictBranchID()) + .getVillageName()); } address.setSubDistrictId(beneficiaryDemographicsModel.getBlockID()); address.setCountryId(beneficiaryDemographicsModel.getCountryID()); if (beneficiaryDemographicsModel.getBlockID() != null) { - address.setSubDistrict( - blockMapper.districtBlockToModelByID(beneficiaryDemographicsModel.getBlockID()).getBlockName()); + address.setSubDistrict(isStopTB + ? nikshayAddressResolver.resolveTUName(beneficiaryDemographicsModel.getBlockID()) + : blockMapper.districtBlockToModelByID(beneficiaryDemographicsModel.getBlockID()).getBlockName()); } address.setGpsLatitude(beneficiaryDemographicsModel.getLatitude()); address.setGpsLongitude(beneficiaryDemographicsModel.getLongitude()); @@ -124,9 +137,13 @@ public IdentityEditDTO BenToIdentityEditMapper(BeneficiaryModel beneficiary) { } IdentityEditDTO identityEditDTO = new IdentityEditDTO(); - identityEditDTO.setPermanentAddress(beneficiaryDemographicsModelToAddress(beneficiary.getI_bendemographics())); - identityEditDTO.setCurrentAddress(beneficiaryDemographicsModelToAddress(beneficiary.getI_bendemographics())); - identityEditDTO.setEmergencyAddress(beneficiaryDemographicsModelToAddress(beneficiary.getI_bendemographics())); + boolean isStopTB = nikshayAddressResolver.isStopTB(beneficiary.getProviderServiceMapID()); + identityEditDTO.setPermanentAddress( + beneficiaryDemographicsModelToAddress(beneficiary.getI_bendemographics(), isStopTB)); + identityEditDTO.setCurrentAddress( + beneficiaryDemographicsModelToAddress(beneficiary.getI_bendemographics(), isStopTB)); + identityEditDTO.setEmergencyAddress( + beneficiaryDemographicsModelToAddress(beneficiary.getI_bendemographics(), isStopTB)); identityEditDTO.setPlaceOfWork(beneficiary.getPlaceOfWork()); identityEditDTO.setMarriageDate(beneficiary.getMarriageDate()); identityEditDTO.setIsHIVPositive(beneficiary.getIsHIVPos()); diff --git a/src/main/java/com/iemr/common/repository/location/NikshayDistrictRepository.java b/src/main/java/com/iemr/common/repository/location/NikshayDistrictRepository.java new file mode 100644 index 00000000..34e202c2 --- /dev/null +++ b/src/main/java/com/iemr/common/repository/location/NikshayDistrictRepository.java @@ -0,0 +1,31 @@ +/* +* 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.common.repository.location; + +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +import com.iemr.common.data.location.NikshayDistrict; + +@Repository +public interface NikshayDistrictRepository extends CrudRepository { +} diff --git a/src/main/java/com/iemr/common/repository/location/NikshayTURepository.java b/src/main/java/com/iemr/common/repository/location/NikshayTURepository.java new file mode 100644 index 00000000..c810613d --- /dev/null +++ b/src/main/java/com/iemr/common/repository/location/NikshayTURepository.java @@ -0,0 +1,31 @@ +/* +* 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.common.repository.location; + +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +import com.iemr.common.data.location.NikshayTU; + +@Repository +public interface NikshayTURepository extends CrudRepository { +} diff --git a/src/main/java/com/iemr/common/repository/location/NikshayVillageRepository.java b/src/main/java/com/iemr/common/repository/location/NikshayVillageRepository.java new file mode 100644 index 00000000..ee89c402 --- /dev/null +++ b/src/main/java/com/iemr/common/repository/location/NikshayVillageRepository.java @@ -0,0 +1,31 @@ +/* +* 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.common.repository.location; + +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +import com.iemr.common.data.location.NikshayVillage; + +@Repository +public interface NikshayVillageRepository extends CrudRepository { +} diff --git a/src/main/java/com/iemr/common/service/location/NikshayAddressResolver.java b/src/main/java/com/iemr/common/service/location/NikshayAddressResolver.java new file mode 100644 index 00000000..96f078fb --- /dev/null +++ b/src/main/java/com/iemr/common/service/location/NikshayAddressResolver.java @@ -0,0 +1,82 @@ +/* +* 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.common.service.location; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.iemr.common.data.location.NikshayDistrict; +import com.iemr.common.data.location.NikshayTU; +import com.iemr.common.data.location.NikshayVillage; +import com.iemr.common.data.users.ProviderServiceMapping; +import com.iemr.common.repository.location.NikshayDistrictRepository; +import com.iemr.common.repository.location.NikshayTURepository; +import com.iemr.common.repository.location.NikshayVillageRepository; +import com.iemr.common.repository.users.ProviderServiceMapRepository; + +/** + * Stop TB workers are mapped against Nikshay's own, isolated location + * hierarchy (m_nikshay_district/tu/village), which uses a separate ID space + * from AMRIT's standard masters (m_district/m_districtblock/m_DistrictBranchMapping). + * The same numeric district/block/village IDs mean different places in each + * hierarchy, so a Stop TB registration must never be resolved against the + * standard masters (see StopTB district/village mismatch investigation, 2026-07-28). + * + * Every other service line (FLW/HWC/MMU) keeps using the standard masters + * untouched — this resolver only changes behavior for Stop TB beneficiaries. + */ +@Component +public class NikshayAddressResolver { + + @Autowired + private ProviderServiceMapRepository providerServiceMapRepository; + @Autowired + private NikshayDistrictRepository nikshayDistrictRepository; + @Autowired + private NikshayTURepository nikshayTURepository; + @Autowired + private NikshayVillageRepository nikshayVillageRepository; + + private static final String STOP_TB_SERVICE_NAME = "Stop TB"; + + public boolean isStopTB(Integer providerServiceMapID) { + if (providerServiceMapID == null) { + return false; + } + ProviderServiceMapping psm = providerServiceMapRepository.findByID(providerServiceMapID); + return psm != null && psm.getM_ServiceMaster() != null + && STOP_TB_SERVICE_NAME.equalsIgnoreCase(psm.getM_ServiceMaster().getServiceName()); + } + + public String resolveDistrictName(Integer nikshayDistrictID) { + return nikshayDistrictRepository.findById(nikshayDistrictID).map(NikshayDistrict::getDistrictName) + .orElse(null); + } + + public String resolveTUName(Integer nikshayTUID) { + return nikshayTURepository.findById(nikshayTUID).map(NikshayTU::getTuName).orElse(null); + } + + public String resolveVillageName(Integer nikshayVillageID) { + return nikshayVillageRepository.findById(nikshayVillageID).map(NikshayVillage::getVillageName).orElse(null); + } +}