From 4191251a20f06589b5c5d248202d7ebc786eb12e Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Fri, 5 Jun 2026 16:15:22 +0530 Subject: [PATCH 01/11] feat: write anthropometry from rmnch/syncDataToAmrit to i_beneficiarydetails.otherFields Mobile sends height/weight/bmi/temperature in beneficiaryDetails payload. i_beneficiarydetails_rmnch has no these columns so they were lost. FLW-API getBeneficiaryData reads from otherFields (temperatureValue key). - RMNCHBeneficiaryDetailsRmnch: @Transient height/weight/bmi/temperature - BenDetailRepo: updateOtherFieldsByBenRegId query - RmnchDataSyncServiceImpl: merge anthropometry into otherFields after save Co-Authored-By: Claude Sonnet 4.6 --- .../rmnch/RMNCHBeneficiaryDetailsRmnch.java | 16 +++++++ .../common/identity/repo/BenDetailRepo.java | 5 +++ .../rmnch/RmnchDataSyncServiceImpl.java | 43 +++++++++++++++++++ 3 files changed, 64 insertions(+) diff --git a/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHBeneficiaryDetailsRmnch.java b/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHBeneficiaryDetailsRmnch.java index 1fb1b66d..e0a89016 100644 --- a/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHBeneficiaryDetailsRmnch.java +++ b/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHBeneficiaryDetailsRmnch.java @@ -557,4 +557,20 @@ public class RMNCHBeneficiaryDetailsRmnch { @Expose @Transient private String familyId; + + // Anthropometry fields sent by Stop TB mobile app via beneficiaryDetails payload. + // i_beneficiarydetails_rmnch has no these columns — stored in i_beneficiarydetails.otherFields instead. + @Expose + @Transient + private Double height; + @Expose + @Transient + private Double weight; + @Expose + @Transient + private Double bmi; + @Expose + @Transient + private Double temperature; // stored as "temperatureValue" in otherFields to match getBeneficiaryData key + } diff --git a/src/main/java/com/iemr/common/identity/repo/BenDetailRepo.java b/src/main/java/com/iemr/common/identity/repo/BenDetailRepo.java index 4e8bcc01..413298ba 100644 --- a/src/main/java/com/iemr/common/identity/repo/BenDetailRepo.java +++ b/src/main/java/com/iemr/common/identity/repo/BenDetailRepo.java @@ -148,6 +148,11 @@ int untagFamily(@Param("modifiedBy") String modifiedBy, @Param("vanSerialNo") Bi @Query("SELECT b FROM MBeneficiarydetail b WHERE b.familyId =:familyid ") List searchByFamilyId(@Param("familyid") String familyid); + @Transactional + @Modifying + @Query("UPDATE MBeneficiarydetail d SET d.otherFields = :otherFields WHERE d.mBeneficiarymapping.benRegId = :benRegId") + int updateOtherFieldsByBenRegId(@Param("benRegId") BigInteger benRegId, @Param("otherFields") String otherFields); + /** * Find complete beneficiary data by IDs from Elasticsearch */ diff --git a/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java b/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java index 3bbd6b09..f7b9d7e1 100644 --- a/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java +++ b/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java @@ -73,6 +73,8 @@ import com.iemr.common.identity.repo.rmnch.RMNCHCBACDetailsRepo; import com.iemr.common.identity.repo.rmnch.RMNCHHouseHoldDetailsRepo; import com.iemr.common.identity.repo.rmnch.RMNCHMBenMappingRepo; +import com.iemr.common.identity.domain.MBeneficiarydetail; +import com.iemr.common.identity.repo.BenDetailRepo; import com.iemr.common.identity.repo.rmnch.RMNCHMBenRegIdMapRepo; import com.iemr.common.identity.utils.config.ConfigProperties; import com.iemr.common.identity.utils.exception.IEMRException; @@ -116,6 +118,9 @@ public class RmnchDataSyncServiceImpl implements RmnchDataSyncService { @Value("${fhir-url}") private String fhirUrl; + + @Autowired + private BenDetailRepo benDetailRepo; @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) @Override public String syncDataToAmrit(String requestOBJ, String authorization) throws Exception { @@ -215,6 +220,22 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex // update beneficiary data in i_beneficiarydetails table rMNCHBenDetailsRepo.saveAll(benDetailsList); + // Write anthropometry (height/weight/bmi/temperature) to i_beneficiarydetails.otherFields. + // i_beneficiarydetails_rmnch has no these columns; FLW-API getBeneficiaryData reads from otherFields. + for (RMNCHBeneficiaryDetailsRmnch obj : benDetailsExtraList) { + if (obj.getBenRegId() != null && hasAnthropometryData(obj)) { + try { + MBeneficiarydetail benDetail = benDetailRepo.findByBenRegId(obj.getBenRegId()); + if (benDetail != null) { + String merged = mergeAnthropometry(benDetail.getOtherFields(), obj); + benDetailRepo.updateOtherFieldsByBenRegId(obj.getBenRegId(), merged); + } + } catch (Exception ex) { + logger.warn("Failed to update otherFields for benRegId: " + obj.getBenRegId() + " - " + ex.getMessage()); + } + } + } + // born birth details if (jsnOBJ != null && jsnOBJ.has("bornBirthDeatils")) { RMNCHBornBirthDetails[] objArr1 = InputMapper.gson() @@ -543,6 +564,28 @@ private Integer getInt(JsonObject obj, String key, Integer defaultVal) { ? obj.get(key).getAsInt() : defaultVal; } + + private boolean hasAnthropometryData(RMNCHBeneficiaryDetailsRmnch obj) { + return obj.getHeight() != null || obj.getWeight() != null + || obj.getBmi() != null || obj.getTemperature() != null; + } + + private String mergeAnthropometry(String existingOtherFields, RMNCHBeneficiaryDetailsRmnch obj) { + JsonObject json = new JsonObject(); + if (existingOtherFields != null && !existingOtherFields.isBlank()) { + try { + json = new JsonParser().parse(existingOtherFields).getAsJsonObject(); + } catch (Exception ignored) { + } + } + if (obj.getHeight() != null) json.addProperty("height", obj.getHeight()); + if (obj.getWeight() != null) json.addProperty("weight", obj.getWeight()); + if (obj.getBmi() != null) json.addProperty("bmi", obj.getBmi()); + // mobile sends "temperature"; FLW-API getBeneficiaryData reads "temperatureValue" + if (obj.getTemperature() != null) json.addProperty("temperatureValue", obj.getTemperature()); + return new Gson().toJson(json); + } + @Override public String getBenData(String requestOBJ, String authorisation) throws Exception { String outputResponse = null; From e09076298bf41c4d83aab8ca5e9a701b8d549e1a Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Fri, 5 Jun 2026 17:56:21 +0530 Subject: [PATCH 02/11] feat: stamp vanID from Redis on all 4 RMNCH entities in syncDataToAmrit Reads camp:vanID and camp:parkingPlaceID from Redis (written by MMU-API on van login) and stamps them onto RMNCHBeneficiaryDetailsRmnch, RMNCHBornBirthDetails, RMNCHCBACdetails, and RMNCHHouseHoldDetails when the mobile payload carries VanID=null or 0. Also writes anthropometry (height/weight/bmi/temperature) from mobile beneficiaryDetails payload into i_beneficiarydetails.otherFields so FLW-API getBeneficiaryData can return them. Gracefully skips if Redis has no camp configured. Co-Authored-By: Claude Sonnet 4.6 --- .../rmnch/RmnchDataSyncServiceImpl.java | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java b/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java index f7b9d7e1..74c2ed57 100644 --- a/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java +++ b/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java @@ -75,6 +75,7 @@ import com.iemr.common.identity.repo.rmnch.RMNCHMBenMappingRepo; import com.iemr.common.identity.domain.MBeneficiarydetail; import com.iemr.common.identity.repo.BenDetailRepo; +import com.iemr.common.identity.utils.redis.RedisStorage; import com.iemr.common.identity.repo.rmnch.RMNCHMBenRegIdMapRepo; import com.iemr.common.identity.utils.config.ConfigProperties; import com.iemr.common.identity.utils.exception.IEMRException; @@ -121,6 +122,8 @@ public class RmnchDataSyncServiceImpl implements RmnchDataSyncService { @Autowired private BenDetailRepo benDetailRepo; + @Autowired + private RedisStorage redisStorage; @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) @Override public String syncDataToAmrit(String requestOBJ, String authorization) throws Exception { @@ -133,6 +136,20 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex ArrayList cBACDetailsIds = new ArrayList<>(); ArrayList houseHoldDetailsIds = new ArrayList<>(); + // Read camp vanID/parkingPlaceID from Redis (set by MMU-API on van login) + Integer campVanID = null; + Integer campParkingPlaceID = null; + try { + String vanVal = redisStorage.getObject("camp:vanID", false, 0); + String ppVal = redisStorage.getObject("camp:parkingPlaceID", false, 0); + if (vanVal != null && !vanVal.isBlank()) campVanID = Integer.parseInt(vanVal); + if (ppVal != null && !ppVal.isBlank()) campParkingPlaceID = Integer.parseInt(ppVal); + } catch (Exception ignored) { + // no camp configured — vanID stamping skipped + } + final Integer vanID = campVanID; + final Integer parkingPlaceID = campParkingPlaceID; + try { if (requestOBJ != null && !requestOBJ.isEmpty()) { JsonObject jsnOBJ = new JsonObject(); @@ -183,6 +200,10 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex } obj.setRelatedBeneficiaryIdsDB(sb.toString()); } + if (obj.getVanID() == null && vanID != null) { + obj.setVanID(vanID); + obj.setParkingPlaceID(parkingPlaceID); + } if(!rMNCHBenDetailsRepo.getByBenRegID(obj.getBenRegId()).isEmpty()){ RMNCHMBeneficiarydetail rmnchmBeneficiarydetail = rMNCHBenDetailsRepo.getByBenRegID(obj.getBenRegId()).get(0); @@ -249,7 +270,10 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex if (temp != null) obj.setBornBirthDeatilsId(temp.getBornBirthDeatilsId()); } - + if (obj.getVanID() == null && vanID != null) { + obj.setVanID(vanID); + obj.setParkingPlaceID(parkingPlaceID); + } } bornBirthList = (ArrayList) rMNCHBornBirthDetailsRepo .saveAll(bornBirthList); @@ -275,7 +299,10 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex if (temp != null) obj.setCBACDetailsid(temp.getCBACDetailsid()); } - + if (obj.getVanID() == null && vanID != null) { + obj.setVanID(vanID); + obj.setParkingPlaceID(parkingPlaceID); + } } cbacList = (ArrayList) rMNCHCBACDetailsRepo.saveAll(cbacList); @@ -296,7 +323,10 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex if (temp != null) obj.setHouseHoldDetailsId(temp.getHouseHoldDetailsId()); } - + if (obj.getVanID() == null && vanID != null) { + obj.setVanID(vanID); + obj.setParkingPlaceID(parkingPlaceID); + } } houseHoldList = (ArrayList) rMNCHHouseHoldDetailsRepo .saveAll(houseHoldList); From 4a05be7c599b68992a296f5294e3cafe12a06571 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Fri, 5 Jun 2026 18:23:47 +0530 Subject: [PATCH 03/11] fix: preserve @Transient anthropometry fields lost after JPA saveAll merge Hibernate merge() returns new managed instances that do not carry @Transient field values (height/weight/bmi/temperature). Keep a reference to the original list before saveAll so the anthropometry loop reads from objects that still have the mobile payload values. Co-Authored-By: Claude Sonnet 4.6 --- .../identity/service/rmnch/RmnchDataSyncServiceImpl.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java b/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java index 74c2ed57..f1850002 100644 --- a/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java +++ b/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java @@ -234,6 +234,9 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex } + // Keep original list before saveAll — @Transient fields (height/weight/bmi/temperature) + // are lost in the JPA-managed instances returned by merge() + List benDetailsOriginalList = new ArrayList<>(benDetailsExtraList); benDetailsExtraList = (ArrayList) rMNCHBeneficiaryDetailsRmnchRepo .saveAll(benDetailsExtraList); @@ -243,7 +246,7 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex // Write anthropometry (height/weight/bmi/temperature) to i_beneficiarydetails.otherFields. // i_beneficiarydetails_rmnch has no these columns; FLW-API getBeneficiaryData reads from otherFields. - for (RMNCHBeneficiaryDetailsRmnch obj : benDetailsExtraList) { + for (RMNCHBeneficiaryDetailsRmnch obj : benDetailsOriginalList) { if (obj.getBenRegId() != null && hasAnthropometryData(obj)) { try { MBeneficiarydetail benDetail = benDetailRepo.findByBenRegId(obj.getBenRegId()); From a3fe65e70736afef640a65f6c26d66e1c8a6e7ca Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Fri, 12 Jun 2026 13:56:10 +0530 Subject: [PATCH 04/11] fix: prevent camp:vanID deletion on every syncDataToAmrit call getObject("camp:vanID", false, 0) was calling EXPIRE key 0 which immediately deletes the key in Redis after every sync. Replaced with getRaw() which reads the value without modifying TTL, so camp:vanID persists across multiple syncs until MMU logout. Co-Authored-By: Claude Sonnet 4.6 --- .../identity/service/rmnch/RmnchDataSyncServiceImpl.java | 4 ++-- .../com/iemr/common/identity/utils/redis/RedisStorage.java | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java b/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java index f1850002..6c0c64f8 100644 --- a/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java +++ b/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java @@ -140,8 +140,8 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex Integer campVanID = null; Integer campParkingPlaceID = null; try { - String vanVal = redisStorage.getObject("camp:vanID", false, 0); - String ppVal = redisStorage.getObject("camp:parkingPlaceID", false, 0); + String vanVal = redisStorage.getRaw("camp:vanID"); + String ppVal = redisStorage.getRaw("camp:parkingPlaceID"); if (vanVal != null && !vanVal.isBlank()) campVanID = Integer.parseInt(vanVal); if (ppVal != null && !ppVal.isBlank()) campParkingPlaceID = Integer.parseInt(ppVal); } catch (Exception ignored) { diff --git a/src/main/java/com/iemr/common/identity/utils/redis/RedisStorage.java b/src/main/java/com/iemr/common/identity/utils/redis/RedisStorage.java index 04a3f1d8..98ced6fa 100644 --- a/src/main/java/com/iemr/common/identity/utils/redis/RedisStorage.java +++ b/src/main/java/com/iemr/common/identity/utils/redis/RedisStorage.java @@ -67,6 +67,13 @@ public String getObject(String key, Boolean extendExpirationTime, int expiration return userRespFromRedis; } + public String getRaw(String key) { + RedisConnection redCon = connection.getConnection(); + byte[] data = redCon.get(key.getBytes()); + redCon.close(); + return data != null ? new String(data) : null; + } + public Long deleteObject(String key) throws RedisSessionException { RedisConnection redCon = connection.getConnection(); Long userRespFromRedis = Long.valueOf(0L); From 5a57dcce84e365c629c57890429688460071a5c9 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Tue, 16 Jun 2026 16:32:08 +0530 Subject: [PATCH 05/11] feat(stoptb): add stoptb.enforce.vanid flag to syncDataToAmrit The Stop TB mobile app calls this RMNCH sync endpoint directly to save household details. Same issue as elsewhere: vanID stamping is skipped silently when Redis has no camp:vanID, so household records end up with vanID=NULL. When stoptb.enforce.vanid=true, sync now fails with a clear error instead. --- src/main/environment/common_ci.properties | 3 +++ src/main/environment/common_docker.properties | 3 +++ src/main/environment/common_example.properties | 5 +++++ .../identity/service/rmnch/RmnchDataSyncServiceImpl.java | 9 +++++++++ 4 files changed, 20 insertions(+) diff --git a/src/main/environment/common_ci.properties b/src/main/environment/common_ci.properties index e32dc366..1ce43664 100644 --- a/src/main/environment/common_ci.properties +++ b/src/main/environment/common_ci.properties @@ -22,6 +22,9 @@ fhir-url=@env.FHIR_API@ # Redis Config spring.redis.host=@env.REDIS_HOST@ +# Stop TB: when true, RMNCH data sync fails with an error if camp (vanID) is not configured +stoptb.enforce.vanid=@env.STOPTB_ENFORCE_VANID@ + cors.allowed-origins=@env.CORS_ALLOWED_ORIGINS@ # Elasticsearch Configuration diff --git a/src/main/environment/common_docker.properties b/src/main/environment/common_docker.properties index 07bd53cc..36735700 100644 --- a/src/main/environment/common_docker.properties +++ b/src/main/environment/common_docker.properties @@ -22,6 +22,9 @@ fhir-url=${FHIR_API} # Redis Config spring.redis.host=${REDIS_HOST} +# Stop TB: when true, RMNCH data sync fails with an error if camp (vanID) is not configured +stoptb.enforce.vanid=${STOPTB_ENFORCE_VANID} + cors.allowed-origins=${CORS_ALLOWED_ORIGINS} # Elasticsearch Configuration diff --git a/src/main/environment/common_example.properties b/src/main/environment/common_example.properties index b78483f2..dd235c95 100644 --- a/src/main/environment/common_example.properties +++ b/src/main/environment/common_example.properties @@ -17,6 +17,11 @@ fhir-url=http://localhost:8093/ # Redis Config spring.redis.host=localhost + +# Stop TB: when true, RMNCH data sync fails with an error if camp (vanID) is not +# configured instead of silently skipping vanID stamping +stoptb.enforce.vanid=false + cors.allowed-origins=http://localhost:* # Elasticsearch Configuration diff --git a/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java b/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java index 6c0c64f8..3f350eb3 100644 --- a/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java +++ b/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java @@ -124,6 +124,11 @@ public class RmnchDataSyncServiceImpl implements RmnchDataSyncService { private BenDetailRepo benDetailRepo; @Autowired private RedisStorage redisStorage; + + // When true, sync fails loudly if camp is not configured instead of silently + // skipping vanID stamping + @Value("${stoptb.enforce.vanid:false}") + private boolean enforceVanID; @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) @Override public String syncDataToAmrit(String requestOBJ, String authorization) throws Exception { @@ -147,6 +152,10 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex } catch (Exception ignored) { // no camp configured — vanID stamping skipped } + if (campVanID == null && enforceVanID) { + throw new Exception( + "Camp not configured: vanID missing. Please select van/service point in MMU before syncing data."); + } final Integer vanID = campVanID; final Integer parkingPlaceID = campParkingPlaceID; From 20c85c7b5e8ab99380ed0a15f32793689c107e99 Mon Sep 17 00:00:00 2001 From: Sehjot Singh Pannu Date: Thu, 18 Jun 2026 17:09:32 +0530 Subject: [PATCH 06/11] feat(STOP-148): add GPS location capture support for beneficiary and RMNCH records Introduces GPS-related fields (gpsLatitude, gpsLongitude, digipin, gpsTimestamp, isGpsUnavailable, and gpsUnavailableReason) across beneficiary address and RMNCH domain models, including MBeneficiaryaddress, Address DTO, RMNCHBeneficiaryDetailsRmnch, and RMNCHHouseHoldDetails. Enhances IdentityMapper and IdentityService to map and persist GPS information between incoming DTOs and beneficiary address entities. Updates RmnchDataSyncServiceImpl to extract and synchronize GPS details from the nested i_bendemographics payload during beneficiary sync, and to parse gpsTimestamp from household details during RMNCH household data processing. --- .../rmnch/RMNCHBeneficiaryDetailsRmnch.java | 24 ++ .../data/rmnch/RMNCHHouseHoldDetails.java | 69 +++-- .../iemr/common/identity/domain/Address.java | 8 + .../identity/domain/MBeneficiaryaddress.java | 60 ++-- .../identity/mapper/IdentityMapper.java | 84 +++--- .../identity/service/IdentityService.java | 262 +++++++++--------- .../rmnch/RmnchDataSyncServiceImpl.java | 48 +++- 7 files changed, 345 insertions(+), 210 deletions(-) diff --git a/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHBeneficiaryDetailsRmnch.java b/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHBeneficiaryDetailsRmnch.java index e0a89016..367d31fa 100644 --- a/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHBeneficiaryDetailsRmnch.java +++ b/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHBeneficiaryDetailsRmnch.java @@ -573,4 +573,28 @@ public class RMNCHBeneficiaryDetailsRmnch { @Transient private Double temperature; // stored as "temperatureValue" in otherFields to match getBeneficiaryData key + @Expose + @Column(name = "gpsLatitude") + private Double gpsLatitude; + + @Expose + @Column(name = "gpsLongitude") + private Double gpsLongitude; + + @Expose + @Column(name = "digipin") + private String digipin; + + @Expose + @Column(name = "gpsTimestamp") + private Timestamp gpsTimestamp; + + @Expose + @Column(name = "isGpsUnavailable", nullable = false, columnDefinition = "TINYINT(1) DEFAULT 0") + private Boolean isGpsUnavailable = false; + + @Expose + @Column(name = "gpsUnavailableReason") + private String gpsUnavailableReason; + } diff --git a/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHHouseHoldDetails.java b/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHHouseHoldDetails.java index 8aa8ed47..b73f9b00 100644 --- a/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHHouseHoldDetails.java +++ b/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHHouseHoldDetails.java @@ -1,24 +1,24 @@ /* -* AMRIT – Accessible Medical Records via Integrated Technology -* Integrated EHR (Electronic Health Records) Solution -* -* Copyright (C) "Piramal Swasthya Management and Research Institute" -* -* This file is part of AMRIT. -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ package com.iemr.common.identity.data.rmnch; import java.sql.Timestamp; @@ -31,11 +31,12 @@ import jakarta.persistence.Table; import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; import lombok.Data; /** - * + * * @author de40034072 * */ @@ -359,4 +360,30 @@ public class RMNCHHouseHoldDetails { @Column(name = "mohallaName") private String mohallaName; + @Expose + @SerializedName("latitude") + @Column(name = "gpsLatitude") + private Double gpsLatitude; + + @Expose + @SerializedName("longitude") + @Column(name = "gpsLongitude") + private Double gpsLongitude; + + @Expose + @Column(name = "digipin") + private String digipin; + + @Expose + @Column(name = "gpsTimestamp") + private Timestamp gpsTimestamp; + + @Expose + @Column(name = "isGpsUnavailable", nullable = false, columnDefinition = "TINYINT(1) DEFAULT 0") + private Boolean isGpsUnavailable = false; + + @Expose + @Column(name = "gpsUnavailableReason") + private String gpsUnavailableReason; + } diff --git a/src/main/java/com/iemr/common/identity/domain/Address.java b/src/main/java/com/iemr/common/identity/domain/Address.java index c37b4507..dfce1672 100644 --- a/src/main/java/com/iemr/common/identity/domain/Address.java +++ b/src/main/java/com/iemr/common/identity/domain/Address.java @@ -23,6 +23,8 @@ import lombok.Data; +import java.sql.Timestamp; + public @Data class Address { private String addrLine1; private String addrLine2; @@ -51,4 +53,10 @@ private Integer vanID; private Integer parkingPlaceID; + private Double gpsLatitude; + private Double gpsLongitude; + private String digipin; + private Timestamp gpsTimestamp; + private Boolean isGpsUnavailable; + private String gpsUnavailableReason; } diff --git a/src/main/java/com/iemr/common/identity/domain/MBeneficiaryaddress.java b/src/main/java/com/iemr/common/identity/domain/MBeneficiaryaddress.java index 0e2ab415..d5eaf7bd 100644 --- a/src/main/java/com/iemr/common/identity/domain/MBeneficiaryaddress.java +++ b/src/main/java/com/iemr/common/identity/domain/MBeneficiaryaddress.java @@ -1,24 +1,24 @@ /* -* AMRIT – Accessible Medical Records via Integrated Technology -* Integrated EHR (Electronic Health Records) Solution -* -* Copyright (C) "Piramal Swasthya Management and Research Institute" -* -* This file is part of AMRIT. -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ package com.iemr.common.identity.domain; import java.io.Serializable; @@ -39,7 +39,7 @@ /** * The persistent class for the m_beneficiaryaddress database table. - * + * */ @Entity @Table(name = "i_beneficiaryaddress") @@ -260,6 +260,24 @@ public class MBeneficiaryaddress implements Serializable { // END OF new column added for data sync + @Column(name = "gpsLatitude") + private Double gpsLatitude; + + @Column(name = "gpsLongitude") + private Double gpsLongitude; + + @Column(name = "digipin") + private String digipin; + + @Column(name = "gpsTimestamp") + private Timestamp gpsTimestamp; + + @Column(name = "isGpsUnavailable", nullable = false, columnDefinition = "TINYINT(1) DEFAULT 0") + private Boolean isGpsUnavailable = false; + + @Column(name = "gpsUnavailableReason") + private String gpsUnavailableReason; + public MBeneficiaryaddress setCurrentAddress(Address address) { this.currAddrLine1 = address.getAddrLine1(); this.currAddrLine2 = address.getAddrLine2(); diff --git a/src/main/java/com/iemr/common/identity/mapper/IdentityMapper.java b/src/main/java/com/iemr/common/identity/mapper/IdentityMapper.java index bc3ad686..765acb97 100644 --- a/src/main/java/com/iemr/common/identity/mapper/IdentityMapper.java +++ b/src/main/java/com/iemr/common/identity/mapper/IdentityMapper.java @@ -1,24 +1,24 @@ /* -* AMRIT – Accessible Medical Records via Integrated Technology -* Integrated EHR (Electronic Health Records) Solution -* -* Copyright (C) "Piramal Swasthya Management and Research Institute" -* -* This file is part of AMRIT. -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ package com.iemr.common.identity.mapper; import java.sql.Timestamp; @@ -66,7 +66,7 @@ public interface IdentityMapper { MBeneficiarymapping identityDTOToMBeneficiarymapping(IdentityDTO dto); - + @Mapping(source = "defaultNo", target = "shareAnonymousWithGovt") @Mapping(source = "defaultNo", target = "shareAnonymousWithMedicalCommunity") @@ -93,7 +93,7 @@ public interface IdentityMapper { MBeneficiaryconsent identityDTOToDefaultMBeneficiaryconsent(IdentityDTO dto, Boolean defaultYes, Boolean defaultNo); - + // @Mapping(source = "dto.areaId", target = "areaId") // @Mapping(source = "dto.beneficiaryRegId", target = "beneficiaryRegID") @@ -144,7 +144,7 @@ public interface IdentityMapper { // @Mapping(source = "dto.vanID", target = "vanID") // @Mapping(source = "dto.parkingPlaceId", target = "parkingPlaceID") // MBeneficiarydetail identityDTOToMBeneficiarydetail(IdentityDTO dto); - + @Mapping(source = "benFamilyDTO.isEmergencyContact", target = "isEmergencyContact") @Mapping(source = "benFamilyDTO.relationshipToSelf", target = "relationshipToSelf") @@ -152,7 +152,7 @@ public interface IdentityMapper { @Mapping(source = "createdBy", target = "createdBy") @Mapping(source = "createdDate", target = "createdDate") MBeneficiaryfamilymapping identityDTOToMBeneficiaryfamilymapping(BenFamilyDTO benFamilyDTO, String createdBy, - Timestamp createdDate); + Timestamp createdDate); List identityDTOListToMBeneficiaryfamilymappingList(List list); @@ -210,6 +210,12 @@ MBeneficiaryfamilymapping identityDTOToMBeneficiaryfamilymapping(BenFamilyDTO be @Mapping(target = "currentAddress.village", source = "map.MBeneficiaryaddress.currVillage") @Mapping(target = "currentAddress.addressValue", source = "map.MBeneficiaryaddress.currAddressValue") @Mapping(target = "currentAddress.pinCode", source = "map.MBeneficiaryaddress.currPinCode") + @Mapping(target = "currentAddress.gpsLatitude", source = "map.MBeneficiaryaddress.gpsLatitude") + @Mapping(target = "currentAddress.gpsLongitude", source = "map.MBeneficiaryaddress.gpsLongitude") + @Mapping(target = "currentAddress.digipin", source = "map.MBeneficiaryaddress.digipin") + @Mapping(target = "currentAddress.gpsTimestamp", source = "map.MBeneficiaryaddress.gpsTimestamp") + @Mapping(target = "currentAddress.isGpsUnavailable", source = "map.MBeneficiaryaddress.isGpsUnavailable") + @Mapping(target = "currentAddress.gpsUnavailableReason", source = "map.MBeneficiaryaddress.gpsUnavailableReason") @Mapping(target = "emergencyAddress.addrLine1", source = "map.MBeneficiaryaddress.emerAddrLine1") @Mapping(target = "emergencyAddress.addrLine2", source = "map.MBeneficiaryaddress.emerAddrLine2") @Mapping(target = "emergencyAddress.addrLine3", source = "map.MBeneficiaryaddress.emerAddrLine3") @@ -296,13 +302,13 @@ MBeneficiaryfamilymapping identityDTOToMBeneficiaryfamilymapping(BenFamilyDTO be @Mapping(target = "beneficiaryDetails.title", source = "map.MBeneficiarydetail.title") @Mapping(target = "beneficiaryDetails.zoneId", source = "map.MBeneficiarydetail.zoneId") @Mapping(target = "contacts", expression = "java( map != null && map.getMBeneficiarycontact() != null && " - + "map.getMBeneficiarydetail() != null ? " - + "Phone.createContactList(map.getMBeneficiarycontact(), " - + "(benRegId != null ? benRegId.toString() : null), " - + "(map.getMBeneficiarydetail().getFirstName() != null ? map.getMBeneficiarydetail().getFirstName() : \"\") + \" \" + " - + "(map.getMBeneficiarydetail().getMiddleName() != null ? map.getMBeneficiarydetail().getMiddleName() : \"\") + \" \" + " - + "(map.getMBeneficiarydetail().getLastName() != null ? map.getMBeneficiarydetail().getLastName() : \"\") " - + ") : null)") + + "map.getMBeneficiarydetail() != null ? " + + "Phone.createContactList(map.getMBeneficiarycontact(), " + + "(map.getBenRegId() != null ? map.getBenRegId().toString() : null), " + + "(map.getMBeneficiarydetail().getFirstName() != null ? map.getMBeneficiarydetail().getFirstName() : \"\") + \" \" + " + + "(map.getMBeneficiarydetail().getMiddleName() != null ? map.getMBeneficiarydetail().getMiddleName() : \"\") + \" \" + " + + "(map.getMBeneficiarydetail().getLastName() != null ? map.getMBeneficiarydetail().getLastName() : \"\") " + + ") : null)") @Mapping(target = "permanentAddress.zoneID", source = "map.MBeneficiaryaddress.permZoneID") @Mapping(target = "permanentAddress.zoneName", source = "map.MBeneficiaryaddress.permZone") @@ -334,13 +340,13 @@ MBeneficiaryfamilymapping identityDTOToMBeneficiaryfamilymapping(BenFamilyDTO be @Mapping(target = "accountNo", source = "map.MBeneficiaryAccount.accountNo") @Mapping(target = "benAccountID", source = "map.benAccountID") @Mapping(target = "ageAtMarriage", expression = "java(map != null && map.getMBeneficiarydetail() != null ? " - + "MBeneficiarydetail.getAgeAtMarriageCalc(map.getMBeneficiarydetail().getDob(), " - + "map.getMBeneficiarydetail().getMarriageDate(), " - + "map.getMBeneficiarydetail().getAgeAtMarriage()) : null)") + + "MBeneficiarydetail.getAgeAtMarriageCalc(map.getMBeneficiarydetail().getDob(), " + + "map.getMBeneficiarydetail().getMarriageDate(), " + + "map.getMBeneficiarydetail().getAgeAtMarriage()) : null)") @Mapping(target = "marriageDate", expression = "java(map != null && map.getMBeneficiarydetail() != null ? " - + "MBeneficiarydetail.getMarriageDateCalc(map.getMBeneficiarydetail().getDob(), " - + "map.getMBeneficiarydetail().getMarriageDate(), " - + "map.getMBeneficiarydetail().getAgeAtMarriage()) : null)") + + "MBeneficiarydetail.getMarriageDateCalc(map.getMBeneficiarydetail().getDob(), " + + "map.getMBeneficiarydetail().getMarriageDate(), " + + "map.getMBeneficiarydetail().getAgeAtMarriage()) : null)") @Mapping(target = "literacyStatus", source = "map.MBeneficiarydetail.literacyStatus") @Mapping(target = "motherName", source = "map.MBeneficiarydetail.motherName") @@ -481,7 +487,7 @@ List mapToMBeneficiaryfamilymappingWithBenFamilyDTOList( @Mapping(source = "dto.createdDate", target = "createdDate") @Mapping(source = "dto.vanID", target = "vanID") @Mapping(source = "dto.parkingPlaceId", target = "parkingPlaceID") - // End + // End MBeneficiaryAccount identityDTOToMBeneficiaryAccount(IdentityDTO dto); @InheritInverseConfiguration @@ -490,7 +496,7 @@ List mapToMBeneficiaryfamilymappingWithBenFamilyDTOList( @Mapping(source = "dto.benImage", target = "benImage") @Mapping(source = "dto.agentName", target = "createdBy") @Mapping(source = "dto.createdDate", target = "createdDate") - + @Mapping(source = "dto.vanID", target = "vanID") @Mapping(source = "dto.parkingPlaceId", target = "parkingPlaceID") diff --git a/src/main/java/com/iemr/common/identity/service/IdentityService.java b/src/main/java/com/iemr/common/identity/service/IdentityService.java index 90e660b9..c94eeacf 100644 --- a/src/main/java/com/iemr/common/identity/service/IdentityService.java +++ b/src/main/java/com/iemr/common/identity/service/IdentityService.java @@ -1,23 +1,23 @@ /* -* AMRIT – Accessible Medical Records via Integrated Technology -* Integrated EHR (Electronic Health Records) Solution -* -* Copyright (C) "Piramal Swasthya Management and Research Institute" -* -* This file is part of AMRIT. -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. */ package com.iemr.common.identity.service; @@ -229,7 +229,7 @@ public List getBeneficiaries(IdentitySearchDTO searchDTO) if (list3.get(i) == null || list3.get(i).getBeneficiaryDetails() == null || list3.get(i).getBeneficiaryDetails().getFirstName() == null || !list3.get(i).getBeneficiaryDetails().getFirstName() - .equalsIgnoreCase(searchDTO.getFirstName())) { + .equalsIgnoreCase(searchDTO.getFirstName())) { list3.remove(i); i--; @@ -241,7 +241,7 @@ public List getBeneficiaries(IdentitySearchDTO searchDTO) if (list3.get(i) == null || list3.get(i).getBeneficiaryDetails() == null || list3.get(i).getBeneficiaryDetails().getLastName() == null || !list3.get(i).getBeneficiaryDetails().getLastName() - .equalsIgnoreCase(searchDTO.getLastName())) { + .equalsIgnoreCase(searchDTO.getLastName())) { list3.remove(i); i--; @@ -286,7 +286,7 @@ public List getBeneficiaries(IdentitySearchDTO searchDTO) if (list3.get(i) == null || list3.get(i).getCurrentAddress() == null || list3.get(i).getCurrentAddress().getDistrictId() == null || !list3.get(i).getCurrentAddress().getDistrictId() - .equals(searchDTO.getCurrentAddress().getDistrictId())) { + .equals(searchDTO.getCurrentAddress().getDistrictId())) { list3.remove(i); i--; @@ -298,7 +298,7 @@ public List getBeneficiaries(IdentitySearchDTO searchDTO) if (list3.get(i) == null || list3.get(i).getCurrentAddress() == null || list3.get(i).getCurrentAddress().getVillageId() == null || !list3.get(i).getCurrentAddress().getVillageId() - .equals(searchDTO.getCurrentAddress().getVillageId())) { + .equals(searchDTO.getCurrentAddress().getVillageId())) { list3.remove(i); i--; @@ -515,7 +515,7 @@ public List getBeneficiariesByBenId(BigInteger benId) /** * - * @param BenRegId + * @param benRegId * @return */ public List getBeneficiariesByBenRegId(BigInteger benRegId) @@ -591,104 +591,104 @@ public List getBeneficiariesByPhoneNum(String phoneNum) } -/** - * Advanced search using Elasticsearch with fallback to database - */ -public Map advancedSearchBeneficiariesES( - String firstName, String middleName, String lastName, Integer genderId, java.util.Date dob, - Integer stateId, Integer districtId, Integer blockId, Integer villageId, - String fatherName, String spouseName, String maritalStatus, String phoneNumber, - String beneficiaryId, String healthId, String aadharNo, - Integer userId, String auth, Boolean is1097) throws Exception { - - try { - logger.info("IdentityService.advancedSearchBeneficiariesES - start"); - logger.info("ES enabled: {}", esEnabled); - - Map response = new HashMap<>(); - - if (esEnabled) { - logger.info("Using Elasticsearch for advanced search"); - - // Call Elasticsearch service - List> esResults = elasticsearchService.advancedSearch( - firstName, middleName, lastName, genderId, dob, stateId, districtId, - blockId, villageId, fatherName, spouseName, maritalStatus, phoneNumber, - beneficiaryId, healthId, aadharNo, userId - ); - - response.put("data", esResults); - response.put("count", esResults.size()); - response.put("source", "elasticsearch"); - - logger.info("ES returned {} results", esResults.size()); - - } else { - logger.info("ES disabled - using database for advanced search"); - - IdentitySearchDTO searchDTO = new IdentitySearchDTO(); - searchDTO.setFirstName(firstName); - searchDTO.setLastName(lastName); - searchDTO.setGenderId(genderId); - searchDTO.setDob(dob != null ? new Timestamp(dob.getTime()) : null); - searchDTO.setFatherName(fatherName); - searchDTO.setSpouseName(spouseName); - searchDTO.setContactNumber(phoneNumber); - - if (beneficiaryId != null && !beneficiaryId.trim().isEmpty()) { - try { - searchDTO.setBeneficiaryId(new BigInteger(beneficiaryId)); - } catch (NumberFormatException e) { - logger.warn("Invalid beneficiaryId format: {}", beneficiaryId); + /** + * Advanced search using Elasticsearch with fallback to database + */ + public Map advancedSearchBeneficiariesES( + String firstName, String middleName, String lastName, Integer genderId, Date dob, + Integer stateId, Integer districtId, Integer blockId, Integer villageId, + String fatherName, String spouseName, String maritalStatus, String phoneNumber, + String beneficiaryId, String healthId, String aadharNo, + Integer userId, String auth, Boolean is1097) throws Exception { + + try { + logger.info("IdentityService.advancedSearchBeneficiariesES - start"); + logger.info("ES enabled: {}", esEnabled); + + Map response = new HashMap<>(); + + if (esEnabled) { + logger.info("Using Elasticsearch for advanced search"); + + // Call Elasticsearch service + List> esResults = elasticsearchService.advancedSearch( + firstName, middleName, lastName, genderId, dob, stateId, districtId, + blockId, villageId, fatherName, spouseName, maritalStatus, phoneNumber, + beneficiaryId, healthId, aadharNo, userId + ); + + response.put("data", esResults); + response.put("count", esResults.size()); + response.put("source", "elasticsearch"); + + logger.info("ES returned {} results", esResults.size()); + + } else { + logger.info("ES disabled - using database for advanced search"); + + IdentitySearchDTO searchDTO = new IdentitySearchDTO(); + searchDTO.setFirstName(firstName); + searchDTO.setLastName(lastName); + searchDTO.setGenderId(genderId); + searchDTO.setDob(dob != null ? new Timestamp(dob.getTime()) : null); + searchDTO.setFatherName(fatherName); + searchDTO.setSpouseName(spouseName); + searchDTO.setContactNumber(phoneNumber); + + if (beneficiaryId != null && !beneficiaryId.trim().isEmpty()) { + try { + searchDTO.setBeneficiaryId(new BigInteger(beneficiaryId)); + } catch (NumberFormatException e) { + logger.warn("Invalid beneficiaryId format: {}", beneficiaryId); + } } + + if (stateId != null || districtId != null || blockId != null || villageId != null) { + Address addressDTO = new Address(); + addressDTO.setStateId(stateId); + addressDTO.setDistrictId(districtId); + addressDTO.setSubDistrictId(blockId); + addressDTO.setVillageId(villageId); + searchDTO.setCurrentAddress(addressDTO); + } + + List dbResults = this.getBeneficiaries(searchDTO); + + List> formattedResults = dbResults.stream() + .map(this::convertBeneficiaryDTOToMap) + .collect(Collectors.toList()); + + response.put("data", formattedResults); + response.put("count", formattedResults.size()); + response.put("source", "database"); + + logger.info("Database returned {} results", formattedResults.size()); } - - if (stateId != null || districtId != null || blockId != null || villageId != null) { - Address addressDTO = new Address(); - addressDTO.setStateId(stateId); - addressDTO.setDistrictId(districtId); - addressDTO.setSubDistrictId(blockId); - addressDTO.setVillageId(villageId); - searchDTO.setCurrentAddress(addressDTO); - } - - List dbResults = this.getBeneficiaries(searchDTO); - - List> formattedResults = dbResults.stream() - .map(this::convertBeneficiaryDTOToMap) - .collect(Collectors.toList()); - - response.put("data", formattedResults); - response.put("count", formattedResults.size()); - response.put("source", "database"); - - logger.info("Database returned {} results", formattedResults.size()); - } - - logger.info("IdentityService.advancedSearchBeneficiariesES - end"); - return response; - - } catch (Exception e) { - logger.error("Advanced search failed: {}", e.getMessage(), e); - throw new Exception("Error in advanced search: " + e.getMessage(), e); - } -}/** - * Convert BeneficiariesDTO to Map format - */ -private Map convertBeneficiaryDTOToMap(BeneficiariesDTO dto) { - try { - ObjectMapper mapper = new ObjectMapper(); - String json = mapper.writeValueAsString(dto); - return mapper.readValue(json, Map.class); - } catch (Exception e) { - logger.error("Error converting DTO to map", e); - return new HashMap<>(); + + logger.info("IdentityService.advancedSearchBeneficiariesES - end"); + return response; + + } catch (Exception e) { + logger.error("Advanced search failed: {}", e.getMessage(), e); + throw new Exception("Error in advanced search: " + e.getMessage(), e); + } + }/** + * Convert BeneficiariesDTO to Map format + */ + private Map convertBeneficiaryDTOToMap(BeneficiariesDTO dto) { + try { + ObjectMapper mapper = new ObjectMapper(); + String json = mapper.writeValueAsString(dto); + return mapper.readValue(json, Map.class); + } catch (Exception e) { + logger.error("Error converting DTO to map", e); + return new HashMap<>(); + } } -} - /** + /** * * * * Search beneficiary by healthID / ABHA address @@ -763,7 +763,7 @@ public List searhBeneficiaryByFamilyId(String familyId) List benDetailsList = detailRepo.searchByFamilyId(familyId); if (benDetailsList == null || benDetailsList.isEmpty()) { - return beneficiaryList; + return beneficiaryList; }else { // considering as of now family creation is possible through facility modules // only @@ -789,7 +789,7 @@ public List searhBeneficiaryByFamilyId(String familyId) } public List searchBeneficiaryByVillageIdAndLastModifyDate(List villageIDs, - Timestamp lastModifiedDate) { + Timestamp lastModifiedDate) { List beneficiaryList = new ArrayList<>(); try { @@ -843,7 +843,7 @@ public List searhBeneficiaryByGovIdentity(String identity) // find benmap ids if (benIdentityList == null || benIdentityList.isEmpty()) { - return beneficiaryList; + return beneficiaryList; }else { for (MBeneficiaryidentity identityObj : benIdentityList) { benMapObjArr.addAll( @@ -1124,7 +1124,7 @@ public void editIdentity(IdentityEditDTO identity) throws MissingMandatoryFields benMapping.getMBeneficiaryaddress().getBenAddressID(), benMapping.getVanID()); // next statement is new one, setting correct beneficiaryDetailsId if (benAddressID != null) { - mbAddr.setBenAddressID(benAddressID); + mbAddr.setBenAddressID(benAddressID); }else { throw new MissingMandatoryFieldsException("Either of vanSerialNO or vanID is missing."); } @@ -1151,7 +1151,7 @@ public void editIdentity(IdentityEditDTO identity) throws MissingMandatoryFields benMapping.getMBeneficiarycontact().getBenContactsID(), benMapping.getVanID()); // next statement is new one, setting correct beneficiaryDetailsId if (benContactsID != null) { - benCon.setBenContactsID(benContactsID); + benCon.setBenContactsID(benContactsID); }else { throw new MissingMandatoryFieldsException("Either of vanSerialNO or vanID is missing."); } @@ -1269,7 +1269,7 @@ public void editIdentity(IdentityEditDTO identity) throws MissingMandatoryFields benMapping.getMBeneficiaryAccount().getBenAccountID(), benMapping.getVanID()); // next statement is new one, setting correct beneficiaryDetailsId if (benAccountID != null) { - beneficiaryAccount.setBenAccountID(benAccountID); + beneficiaryAccount.setBenAccountID(benAccountID); }else { throw new MissingMandatoryFieldsException("Either of vanSerialNO or vanID is missing."); } @@ -1294,7 +1294,7 @@ public void editIdentity(IdentityEditDTO identity) throws MissingMandatoryFields benMapping.getMBeneficiaryImage().getBenImageId(), benMapping.getVanID()); // next statement is new one, setting correct beneficiaryDetailsId if (benImageId != null) { - beneficiaryImage.setBenImageId(benImageId); + beneficiaryImage.setBenImageId(benImageId); }else { throw new MissingMandatoryFieldsException("Either of vanSerialNO or vanID is missing."); } @@ -1314,11 +1314,11 @@ public void editIdentity(IdentityEditDTO identity) throws MissingMandatoryFields logger.info("Triggering Elasticsearch sync for benRegId: {}", identity.getBeneficiaryRegId()); syncService.syncBeneficiaryAsync(identity.getBeneficiaryRegId()); } - - logger.info("IdentityService.editIdentity - end. id = " + benMapping.getBenMapId()); -} - + logger.info("IdentityService.editIdentity - end. id = " + benMapping.getBenMapId()); + } + + private MBeneficiarydetail convertIdentityEditDTOToMBeneficiarydetail(IdentityEditDTO dto) { MBeneficiarydetail beneficiarydetail = new MBeneficiarydetail(); @@ -1829,6 +1829,12 @@ private MBeneficiaryaddress identityDTOToMBeneficiaryaddress(IdentityDTO dto) { beneficiaryAddress.setCurrServicePointId(dto.getCurrentAddress().getServicePointID()); beneficiaryAddress.setCurrServicePoint(dto.getCurrentAddress().getServicePointName()); beneficiaryAddress.setCurrHabitation(dto.getCurrentAddress().getHabitation()); + beneficiaryAddress.setGpsLatitude(dto.getCurrentAddress().getGpsLatitude()); + beneficiaryAddress.setGpsLongitude(dto.getCurrentAddress().getGpsLongitude()); + beneficiaryAddress.setDigipin(dto.getCurrentAddress().getDigipin()); + beneficiaryAddress.setGpsTimestamp(dto.getCurrentAddress().getGpsTimestamp()); + beneficiaryAddress.setIsGpsUnavailable(dto.getCurrentAddress().getIsGpsUnavailable()); + beneficiaryAddress.setGpsUnavailableReason(dto.getCurrentAddress().getGpsUnavailableReason()); } if (dto.getEmergencyAddress() != null) { beneficiaryAddress.setEmerAddrLine1(dto.getEmergencyAddress().getAddrLine1()); @@ -1939,7 +1945,7 @@ public String unReserveIdentity(ReserveIdentityDTO unReserve) { * Get partial details of beneficiaries (first name middle name and last * name) list on benId's list * - * @param BenRegIds + * @param benRegIds * @return {@link List} Beneficiaries */ public List getBeneficiariesPartialDeatilsByBenRegIdList(List benRegIds) { diff --git a/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java b/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java index 3f350eb3..0362420c 100644 --- a/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java +++ b/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java @@ -46,6 +46,7 @@ import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import com.google.gson.Gson; +import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; @@ -182,10 +183,37 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex // benRegID = rMNCHMBenRegIdMapRepo.getRegID(benDetailsExtraList.get(0).getBenficieryid()); // // if (benRegID != null) { - + + // Build GPS lookup map from i_bendemographics in raw JSON + Map benGpsMap = new HashMap<>(); + JsonArray benJsonArr = jsnOBJ.getAsJsonArray("beneficiaryDetails"); + for (JsonElement el : benJsonArr) { + JsonObject benJson = el.getAsJsonObject(); + if (benJson.has("benficieryid") && !benJson.get("benficieryid").isJsonNull() + && benJson.has("i_bendemographics") + && !benJson.get("i_bendemographics").isJsonNull()) { + benGpsMap.put(benJson.get("benficieryid").getAsBigInteger(), + benJson.getAsJsonObject("i_bendemographics")); + } + } + for (RMNCHBeneficiaryDetailsRmnch obj : benDetailsExtraList) { benRegID = rMNCHMBenRegIdMapRepo.getRegID(obj.getBenficieryid()); obj.setBenRegId(benRegID); + // Extract GPS from i_bendemographics + JsonObject demog = benGpsMap.get(obj.getBenficieryid()); + if (demog != null) { + if (demog.has("latitude") && !demog.get("latitude").isJsonNull()) + obj.setGpsLatitude(demog.get("latitude").getAsDouble()); + if (demog.has("longitude") && !demog.get("longitude").isJsonNull()) + obj.setGpsLongitude(demog.get("longitude").getAsDouble()); + if (demog.has("digipin") && !demog.get("digipin").isJsonNull()) + obj.setDigipin(demog.get("digipin").getAsString()); + if (demog.has("gpsTimestamp") && !demog.get("gpsTimestamp").isJsonNull()) + obj.setGpsTimestamp(new Timestamp(demog.get("gpsTimestamp").getAsLong())); + if (demog.has("isGpsUnavailable") && !demog.get("isGpsUnavailable").isJsonNull()) + obj.setIsGpsUnavailable(demog.get("isGpsUnavailable").getAsBoolean()); + } if(!rMNCHBeneficiaryDetailsRmnchRepo .getByRegID(benRegID).isEmpty()){ RMNCHBeneficiaryDetailsRmnch temp = rMNCHBeneficiaryDetailsRmnchRepo @@ -327,6 +355,22 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex .fromJson(jsnOBJ.get("houseHoldDetails"), RMNCHHouseHoldDetails[].class); List houseHoldList = Arrays.asList(objArr3); + // Build gpsTimestamp map (sent as string, needs manual parse) + Map hhTimestampMap = new HashMap<>(); + JsonArray hhJsonArr = jsnOBJ.getAsJsonArray("houseHoldDetails"); + for (JsonElement el : hhJsonArr) { + JsonObject hhJson = el.getAsJsonObject(); + try { + if (hhJson.has("houseoldId") && !hhJson.get("houseoldId").isJsonNull() + && hhJson.has("gpsTimestamp") + && !hhJson.get("gpsTimestamp").isJsonNull()) { + hhTimestampMap.put( + Long.parseLong(hhJson.get("houseoldId").getAsString()), + hhJson.get("gpsTimestamp").getAsLong()); + } + } catch (NumberFormatException ignored) {} + } + for (RMNCHHouseHoldDetails obj : houseHoldList) { if(!rMNCHHouseHoldDetailsRepo .getByHouseHoldID(obj.getHouseoldId()).isEmpty()){ @@ -334,6 +378,8 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex .getByHouseHoldID(obj.getHouseoldId()).get(0); if (temp != null) obj.setHouseHoldDetailsId(temp.getHouseHoldDetailsId()); + if (hhTimestampMap.containsKey(obj.getHouseoldId())) + obj.setGpsTimestamp(new Timestamp(hhTimestampMap.get(obj.getHouseoldId()))); } if (obj.getVanID() == null && vanID != null) { obj.setVanID(vanID); From 0c9f67bc753ccc9e9fdf81c359c231308713f9ec Mon Sep 17 00:00:00 2001 From: Vishwanath Balkur <118195001+vishwab1@users.noreply.github.com> Date: Fri, 19 Jun 2026 09:07:54 +0530 Subject: [PATCH 07/11] feat(STOP-148): add GPS location capture support for beneficiary and RMNCH records (#167) Introduces GPS-related fields (gpsLatitude, gpsLongitude, digipin, gpsTimestamp, isGpsUnavailable, and gpsUnavailableReason) across beneficiary address and RMNCH domain models, including MBeneficiaryaddress, Address DTO, RMNCHBeneficiaryDetailsRmnch, and RMNCHHouseHoldDetails. Enhances IdentityMapper and IdentityService to map and persist GPS information between incoming DTOs and beneficiary address entities. Updates RmnchDataSyncServiceImpl to extract and synchronize GPS details from the nested i_bendemographics payload during beneficiary sync, and to parse gpsTimestamp from household details during RMNCH household data processing. Co-authored-by: Sehjot Singh Pannu --- .../common/identity/mapper/InputMapper.java | 43 ++++++++++++++++++- .../identity/utils/mapper/InputMapper.java | 35 +++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/iemr/common/identity/mapper/InputMapper.java b/src/main/java/com/iemr/common/identity/mapper/InputMapper.java index 9a46ba4f..704fe671 100644 --- a/src/main/java/com/iemr/common/identity/mapper/InputMapper.java +++ b/src/main/java/com/iemr/common/identity/mapper/InputMapper.java @@ -21,6 +21,13 @@ */ package com.iemr.common.identity.mapper; +import java.io.IOException; +import java.sql.Timestamp; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Locale; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,6 +40,10 @@ import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; import com.google.gson.LongSerializationPolicy; +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; import com.iemr.common.identity.exception.IEMRException; public class InputMapper @@ -42,11 +53,41 @@ public class InputMapper private static GsonBuilder builder; private static InputMapper instance = null; + private static final DateTimeFormatter ISO_WITH_Z = + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); + private static final DateTimeFormatter CLIENT_DATE_FORMAT = + DateTimeFormatter.ofPattern("MMM dd, yyyy, h:mm:ss a", Locale.ENGLISH); + + private static final TypeAdapter TIMESTAMP_ADAPTER = new TypeAdapter() { + @Override + public void write(JsonWriter out, Timestamp value) throws IOException { + if (value == null) out.nullValue(); + else out.value(value.getTime()); + } + + @Override + public Timestamp read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { in.nextNull(); return null; } + if (in.peek() == JsonToken.NUMBER) return new Timestamp(in.nextLong()); + String s = in.nextString(); + // epoch millis as string + try { return new Timestamp(Long.parseLong(s)); } catch (NumberFormatException ignored) {} + // ISO 8601 with Z e.g. "2021-06-18T00:00:00.000Z" + try { return Timestamp.from(Instant.parse(s)); } catch (Exception ignored) {} + // Mobile client format e.g. "Jun 18, 2021, 5:30:00 AM" + try { return Timestamp.valueOf(LocalDateTime.parse(s, CLIENT_DATE_FORMAT)); } catch (Exception ignored) {} + // Fallback: "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" literal Z + try { return Timestamp.valueOf(LocalDateTime.parse(s, ISO_WITH_Z)); } catch (Exception ignored) {} + return null; + } + }; + private InputMapper() { builder = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") // .excludeFieldsWithoutExposeAnnotation() - .serializeNulls().setLongSerializationPolicy(LongSerializationPolicy.STRING); + .serializeNulls().setLongSerializationPolicy(LongSerializationPolicy.STRING) + .registerTypeAdapter(Timestamp.class, TIMESTAMP_ADAPTER); } public static InputMapper getInstance() diff --git a/src/main/java/com/iemr/common/identity/utils/mapper/InputMapper.java b/src/main/java/com/iemr/common/identity/utils/mapper/InputMapper.java index 4c8db37d..fb419105 100644 --- a/src/main/java/com/iemr/common/identity/utils/mapper/InputMapper.java +++ b/src/main/java/com/iemr/common/identity/utils/mapper/InputMapper.java @@ -21,6 +21,13 @@ */ package com.iemr.common.identity.utils.mapper; +import java.io.IOException; +import java.sql.Timestamp; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @@ -28,6 +35,10 @@ import com.google.gson.ExclusionStrategy; import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; import com.iemr.common.identity.utils.exception.IEMRException; /** @@ -48,6 +59,30 @@ public InputMapper() { if (builder == null) { builder = new GsonBuilder(); builder.setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"); + builder.registerTypeAdapter(Timestamp.class, new TypeAdapter() { + private final DateTimeFormatter isoWithZ = DateTimeFormatter.ISO_INSTANT; + private final DateTimeFormatter isoNoTz = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS"); + + @Override + public void write(JsonWriter out, Timestamp value) throws IOException { + if (value == null) out.nullValue(); + else out.value(value.getTime()); + } + + @Override + public Timestamp read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { in.nextNull(); return null; } + if (in.peek() == JsonToken.NUMBER) return new Timestamp(in.nextLong()); + String s = in.nextString(); + // epoch millis as string + try { return new Timestamp(Long.parseLong(s)); } catch (NumberFormatException ignored) {} + // ISO 8601 with timezone (e.g. "2026-05-28T03:24:35.000Z") + try { return Timestamp.from(Instant.parse(s)); } catch (Exception ignored) {} + // ISO without timezone (e.g. "2026-05-28T03:24:35.000") + try { return Timestamp.from(LocalDateTime.parse(s, isoNoTz).toInstant(ZoneOffset.UTC)); } catch (Exception ignored) {} + return null; + } + }); } } From afaaf77e2d9b699d6f1a6236d68efc397161c26e Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Mon, 22 Jun 2026 19:10:05 +0530 Subject: [PATCH 08/11] fix(STOP-148): use InputMapper Gson for createIdentity to parse epoch-millis gpsTimestamp createIdentity() parsed the request body with a bare new Gson(), whose default Timestamp adapter only accepts ISO8601 date strings. The new gpsTimestamp field is sent as raw epoch millis, causing JsonSyntaxException during parsing whenever GPS data is present. That exception propagated up through JwtUserIdValidationFilter's catch block and was misreported as a 401 Authorization error. InputMapper already registers a Timestamp adapter that handles epoch millis correctly; switch createIdentity() to use it. Co-Authored-By: Claude Sonnet 4.6 --- .../com/iemr/common/identity/controller/IdentityController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/iemr/common/identity/controller/IdentityController.java b/src/main/java/com/iemr/common/identity/controller/IdentityController.java index fc894761..6ca92409 100644 --- a/src/main/java/com/iemr/common/identity/controller/IdentityController.java +++ b/src/main/java/com/iemr/common/identity/controller/IdentityController.java @@ -608,7 +608,7 @@ public String createIdentity(@Param(value = "{\r\n" + " \"eventTypeName\": \"St + " \"createdDate\": \"Timestamp\"\r\n" + " \"faceEmbedding\": [\"Float\"]\r\n" + "}") @RequestBody String identityData) throws IEMRException { logger.info("IdentityController.createIdentity - start"); - IdentityDTO identity = new Gson().fromJson(identityData, IdentityDTO.class); + IdentityDTO identity = InputMapper.getInstance().gson().fromJson(identityData, IdentityDTO.class); logger.info("identity hit: " + identity); BeneficiaryCreateResp map; map = svc.createIdentity(identity); From 725b9540bc3252eba5f8623774eac4b24a759a1c Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Tue, 30 Jun 2026 10:40:13 +0530 Subject: [PATCH 09/11] fix(datasync): add vanID column to BenGenID import INSERT query SQL had 6 placeholders but object array had 7 elements including vanID, causing parameter index out of bounds error. Added vanID to INSERT columns. Co-Authored-By: Claude Sonnet 4.6 --- .../java/com/iemr/common/identity/service/IdentityService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/iemr/common/identity/service/IdentityService.java b/src/main/java/com/iemr/common/identity/service/IdentityService.java index c94eeacf..bb1dd988 100644 --- a/src/main/java/com/iemr/common/identity/service/IdentityService.java +++ b/src/main/java/com/iemr/common/identity/service/IdentityService.java @@ -2163,7 +2163,7 @@ public int importBenIdToLocalServer(List benIdImportDTOList) { List dataList = new ArrayList<>(); Object[] objArr; String query = " INSERT INTO m_beneficiaryregidmapping(BenRegId, BeneficiaryID, " - + " Provisioned, CreatedDate, CreatedBy, Reserved) VALUES (?,?,?,?,?,?) "; + + " Provisioned, CreatedDate, CreatedBy, Reserved, vanID) VALUES (?,?,?,?,?,?,?) "; logger.info("query : " + query); for (MBeneficiaryregidmapping obj : mBeneficiaryregidmappingList) { logger.info("inside for check->", obj); From 5e5a5a73f8c28134db69763459282f5266b06353 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Tue, 7 Jul 2026 13:48:00 +0530 Subject: [PATCH 10/11] fix(rmnch): surface pinCode in beneficiary details sync response permPinCode was already stored correctly in i_beneficiaryaddress but was never surfaced in the RMNCH sync response, since the response DTO had no pinCode field and the address mapping never copied it across. Co-Authored-By: Claude Sonnet 5 --- .../identity/data/rmnch/RMNCHBeneficiaryDetailsRmnch.java | 3 +++ .../identity/service/rmnch/RmnchDataSyncServiceImpl.java | 2 ++ 2 files changed, 5 insertions(+) diff --git a/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHBeneficiaryDetailsRmnch.java b/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHBeneficiaryDetailsRmnch.java index 367d31fa..fb7b1e75 100644 --- a/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHBeneficiaryDetailsRmnch.java +++ b/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHBeneficiaryDetailsRmnch.java @@ -390,6 +390,9 @@ public class RMNCHBeneficiaryDetailsRmnch { @Expose @Transient private String addressLine3; + @Expose + @Transient + private String pinCode; // ---------------------------------------------- diff --git a/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java b/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java index 0362420c..528310d4 100644 --- a/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java +++ b/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java @@ -920,6 +920,8 @@ private String getMappingsForAddressIDs(List addressLi benDetailsRMNCHOBJ.setAddressLine2(benAddressOBJ.getPermAddrLine2()); if (benAddressOBJ.getPermAddrLine3() != null) benDetailsRMNCHOBJ.setAddressLine3(benAddressOBJ.getPermAddrLine3()); + if (benAddressOBJ.getPermPinCode() != null) + benDetailsRMNCHOBJ.setPinCode(benAddressOBJ.getPermPinCode()); // related benids if (benDetailsRMNCHOBJ.getRelatedBeneficiaryIdsDB() != null) { From c2a91d967d35e5011fc45814bebbf50f3c4a8220 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Sat, 18 Jul 2026 17:54:10 +0530 Subject: [PATCH 11/11] fix(dob): stop global Timestamp adapter from nulling dob on GPS-enabled endpoints The GPS feature (STOP-148) registered a custom Gson TypeAdapter globally on InputMapper's GsonBuilder to parse the new gpsTimestamp field. Because it was global, it also intercepted dob, silently returning null whenever the incoming date string didn't match one of its four hardcoded formats. - Add GpsTimestampAdapter, attached only via @JsonAdapter on the gpsTimestamp field (Address, RMNCHBeneficiaryDetailsRmnch, RMNCHHouseHoldDetails), so it can't affect any other Timestamp field. - Remove the global registerTypeAdapter(Timestamp.class, ...) from both InputMapper.java copies, restoring Gson's default Timestamp parsing for dob and everything else (matching vb/stoptb). - Revert createIdentity()'s parser back to a bare new Gson(), matching Common-API's RegisterBenificiaryServiceImpl, which also serializes the outgoing identity payload with a bare new Gson(). InputMapper's setDateFormat is incompatible with that wire format (non-zero-padded day), which is why dob was nulling specifically for beneficiaries born on the 1st-9th of a month once createIdentity switched to InputMapper's Gson. Co-Authored-By: Claude Sonnet 5 --- .../controller/IdentityController.java | 7 +- .../rmnch/RMNCHBeneficiaryDetailsRmnch.java | 3 + .../data/rmnch/RMNCHHouseHoldDetails.java | 3 + .../iemr/common/identity/domain/Address.java | 4 + .../identity/mapper/GpsTimestampAdapter.java | 116 ++++++++++++++++++ .../common/identity/mapper/InputMapper.java | 47 +------ .../identity/utils/mapper/InputMapper.java | 40 +----- 7 files changed, 142 insertions(+), 78 deletions(-) create mode 100644 src/main/java/com/iemr/common/identity/mapper/GpsTimestampAdapter.java diff --git a/src/main/java/com/iemr/common/identity/controller/IdentityController.java b/src/main/java/com/iemr/common/identity/controller/IdentityController.java index 6ca92409..8d015bd1 100644 --- a/src/main/java/com/iemr/common/identity/controller/IdentityController.java +++ b/src/main/java/com/iemr/common/identity/controller/IdentityController.java @@ -608,7 +608,12 @@ public String createIdentity(@Param(value = "{\r\n" + " \"eventTypeName\": \"St + " \"createdDate\": \"Timestamp\"\r\n" + " \"faceEmbedding\": [\"Float\"]\r\n" + "}") @RequestBody String identityData) throws IEMRException { logger.info("IdentityController.createIdentity - start"); - IdentityDTO identity = InputMapper.getInstance().gson().fromJson(identityData, IdentityDTO.class); + // Bare Gson matches Common-API's RegisterBenificiaryServiceImpl, which also + // serializes the outgoing identity payload with a bare new Gson(). dob relies + // on this symmetric default format; gpsTimestamp is still parsed correctly via + // its field-level @JsonAdapter(GpsTimestampAdapter.class) on Address, which + // works regardless of which Gson instance performs the parse. + IdentityDTO identity = new Gson().fromJson(identityData, IdentityDTO.class); logger.info("identity hit: " + identity); BeneficiaryCreateResp map; map = svc.createIdentity(identity); diff --git a/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHBeneficiaryDetailsRmnch.java b/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHBeneficiaryDetailsRmnch.java index fb7b1e75..814d2c73 100644 --- a/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHBeneficiaryDetailsRmnch.java +++ b/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHBeneficiaryDetailsRmnch.java @@ -34,6 +34,8 @@ import jakarta.persistence.Transient; import com.google.gson.annotations.Expose; +import com.google.gson.annotations.JsonAdapter; +import com.iemr.common.identity.mapper.GpsTimestampAdapter; import lombok.Data; @@ -590,6 +592,7 @@ public class RMNCHBeneficiaryDetailsRmnch { @Expose @Column(name = "gpsTimestamp") + @JsonAdapter(GpsTimestampAdapter.class) private Timestamp gpsTimestamp; @Expose diff --git a/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHHouseHoldDetails.java b/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHHouseHoldDetails.java index b73f9b00..7000f6d0 100644 --- a/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHHouseHoldDetails.java +++ b/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHHouseHoldDetails.java @@ -31,7 +31,9 @@ import jakarta.persistence.Table; import com.google.gson.annotations.Expose; +import com.google.gson.annotations.JsonAdapter; import com.google.gson.annotations.SerializedName; +import com.iemr.common.identity.mapper.GpsTimestampAdapter; import lombok.Data; @@ -376,6 +378,7 @@ public class RMNCHHouseHoldDetails { @Expose @Column(name = "gpsTimestamp") + @JsonAdapter(GpsTimestampAdapter.class) private Timestamp gpsTimestamp; @Expose diff --git a/src/main/java/com/iemr/common/identity/domain/Address.java b/src/main/java/com/iemr/common/identity/domain/Address.java index dfce1672..f355cbc8 100644 --- a/src/main/java/com/iemr/common/identity/domain/Address.java +++ b/src/main/java/com/iemr/common/identity/domain/Address.java @@ -25,6 +25,9 @@ import java.sql.Timestamp; +import com.google.gson.annotations.JsonAdapter; +import com.iemr.common.identity.mapper.GpsTimestampAdapter; + public @Data class Address { private String addrLine1; private String addrLine2; @@ -56,6 +59,7 @@ private Double gpsLatitude; private Double gpsLongitude; private String digipin; + @JsonAdapter(GpsTimestampAdapter.class) private Timestamp gpsTimestamp; private Boolean isGpsUnavailable; private String gpsUnavailableReason; diff --git a/src/main/java/com/iemr/common/identity/mapper/GpsTimestampAdapter.java b/src/main/java/com/iemr/common/identity/mapper/GpsTimestampAdapter.java new file mode 100644 index 00000000..1f1fe2ff --- /dev/null +++ b/src/main/java/com/iemr/common/identity/mapper/GpsTimestampAdapter.java @@ -0,0 +1,116 @@ +/* +* 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.identity.mapper; + +import java.io.IOException; +import java.sql.Timestamp; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.Locale; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +/** + * Parses the GPS capture timestamp (epoch millis, ISO-8601 with/without a literal + * 'Z', or the mobile client's "MMM dd, yyyy, h:mm:ss a" format). + * + * Attach with {@code @JsonAdapter(GpsTimestampAdapter.class)} directly on a + * gpsTimestamp field only. Do NOT register this globally on a GsonBuilder for + * Timestamp.class — that previously intercepted every Timestamp field in the + * request (including dob) and silently nulled it out whenever the client's + * format didn't match one of the patterns below. + */ +public class GpsTimestampAdapter extends TypeAdapter { + + private static final Logger logger = LoggerFactory.getLogger(GpsTimestampAdapter.class); + + private static final DateTimeFormatter ISO_WITH_Z = + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); + private static final DateTimeFormatter ISO_NO_TZ = + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS"); + private static final DateTimeFormatter CLIENT_DATE_FORMAT = + DateTimeFormatter.ofPattern("MMM dd, yyyy, h:mm:ss a", Locale.ENGLISH); + + @Override + public void write(JsonWriter out, Timestamp value) throws IOException { + if (value == null) { + out.nullValue(); + } else { + out.value(value.getTime()); + } + } + + @Override + public Timestamp read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + if (in.peek() == JsonToken.NUMBER) { + return new Timestamp(in.nextLong()); + } + + String s = in.nextString(); + + // epoch millis as string + try { + return new Timestamp(Long.parseLong(s)); + } catch (NumberFormatException ignored) { + // not epoch millis, try the date formats below + } + // ISO 8601 with Z, e.g. "2021-06-18T00:00:00.000Z" + try { + return Timestamp.from(Instant.parse(s)); + } catch (Exception ignored) { + // not this format + } + // Mobile client format, e.g. "Jun 18, 2021, 5:30:00 AM" + try { + return Timestamp.valueOf(LocalDateTime.parse(s, CLIENT_DATE_FORMAT)); + } catch (Exception ignored) { + // not this format + } + // ISO with literal 'Z' pattern, e.g. "2021-06-18T00:00:00.000Z" parsed as local + try { + return Timestamp.valueOf(LocalDateTime.parse(s, ISO_WITH_Z)); + } catch (Exception ignored) { + // not this format + } + // ISO without timezone, e.g. "2021-06-18T00:00:00.000" (assume UTC) + try { + return Timestamp.from(LocalDateTime.parse(s, ISO_NO_TZ).toInstant(ZoneOffset.UTC)); + } catch (Exception ignored) { + // not this format + } + + logger.warn("GpsTimestampAdapter: unable to parse gpsTimestamp value '{}' with any known format; storing as null", s); + return null; + } +} diff --git a/src/main/java/com/iemr/common/identity/mapper/InputMapper.java b/src/main/java/com/iemr/common/identity/mapper/InputMapper.java index 704fe671..3549efc2 100644 --- a/src/main/java/com/iemr/common/identity/mapper/InputMapper.java +++ b/src/main/java/com/iemr/common/identity/mapper/InputMapper.java @@ -21,13 +21,6 @@ */ package com.iemr.common.identity.mapper; -import java.io.IOException; -import java.sql.Timestamp; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.Locale; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,10 +33,6 @@ import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; import com.google.gson.LongSerializationPolicy; -import com.google.gson.TypeAdapter; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonToken; -import com.google.gson.stream.JsonWriter; import com.iemr.common.identity.exception.IEMRException; public class InputMapper @@ -53,41 +42,15 @@ public class InputMapper private static GsonBuilder builder; private static InputMapper instance = null; - private static final DateTimeFormatter ISO_WITH_Z = - DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); - private static final DateTimeFormatter CLIENT_DATE_FORMAT = - DateTimeFormatter.ofPattern("MMM dd, yyyy, h:mm:ss a", Locale.ENGLISH); - - private static final TypeAdapter TIMESTAMP_ADAPTER = new TypeAdapter() { - @Override - public void write(JsonWriter out, Timestamp value) throws IOException { - if (value == null) out.nullValue(); - else out.value(value.getTime()); - } - - @Override - public Timestamp read(JsonReader in) throws IOException { - if (in.peek() == JsonToken.NULL) { in.nextNull(); return null; } - if (in.peek() == JsonToken.NUMBER) return new Timestamp(in.nextLong()); - String s = in.nextString(); - // epoch millis as string - try { return new Timestamp(Long.parseLong(s)); } catch (NumberFormatException ignored) {} - // ISO 8601 with Z e.g. "2021-06-18T00:00:00.000Z" - try { return Timestamp.from(Instant.parse(s)); } catch (Exception ignored) {} - // Mobile client format e.g. "Jun 18, 2021, 5:30:00 AM" - try { return Timestamp.valueOf(LocalDateTime.parse(s, CLIENT_DATE_FORMAT)); } catch (Exception ignored) {} - // Fallback: "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" literal Z - try { return Timestamp.valueOf(LocalDateTime.parse(s, ISO_WITH_Z)); } catch (Exception ignored) {} - return null; - } - }; - private InputMapper() { + // Timestamp fields (including dob) use Gson's default parsing here, same as + // on vb/stoptb. The gpsTimestamp field on Address/RMNCH entities is parsed by + // GpsTimestampAdapter via a field-level @JsonAdapter annotation instead of a + // global registration, so it can't affect any other Timestamp field. builder = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") // .excludeFieldsWithoutExposeAnnotation() - .serializeNulls().setLongSerializationPolicy(LongSerializationPolicy.STRING) - .registerTypeAdapter(Timestamp.class, TIMESTAMP_ADAPTER); + .serializeNulls().setLongSerializationPolicy(LongSerializationPolicy.STRING); } public static InputMapper getInstance() diff --git a/src/main/java/com/iemr/common/identity/utils/mapper/InputMapper.java b/src/main/java/com/iemr/common/identity/utils/mapper/InputMapper.java index fb419105..6dbc0301 100644 --- a/src/main/java/com/iemr/common/identity/utils/mapper/InputMapper.java +++ b/src/main/java/com/iemr/common/identity/utils/mapper/InputMapper.java @@ -21,13 +21,6 @@ */ package com.iemr.common.identity.utils.mapper; -import java.io.IOException; -import java.sql.Timestamp; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.time.format.DateTimeFormatter; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @@ -35,10 +28,6 @@ import com.google.gson.ExclusionStrategy; import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; -import com.google.gson.TypeAdapter; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonToken; -import com.google.gson.stream.JsonWriter; import com.iemr.common.identity.utils.exception.IEMRException; /** @@ -59,30 +48,11 @@ public InputMapper() { if (builder == null) { builder = new GsonBuilder(); builder.setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"); - builder.registerTypeAdapter(Timestamp.class, new TypeAdapter() { - private final DateTimeFormatter isoWithZ = DateTimeFormatter.ISO_INSTANT; - private final DateTimeFormatter isoNoTz = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS"); - - @Override - public void write(JsonWriter out, Timestamp value) throws IOException { - if (value == null) out.nullValue(); - else out.value(value.getTime()); - } - - @Override - public Timestamp read(JsonReader in) throws IOException { - if (in.peek() == JsonToken.NULL) { in.nextNull(); return null; } - if (in.peek() == JsonToken.NUMBER) return new Timestamp(in.nextLong()); - String s = in.nextString(); - // epoch millis as string - try { return new Timestamp(Long.parseLong(s)); } catch (NumberFormatException ignored) {} - // ISO 8601 with timezone (e.g. "2026-05-28T03:24:35.000Z") - try { return Timestamp.from(Instant.parse(s)); } catch (Exception ignored) {} - // ISO without timezone (e.g. "2026-05-28T03:24:35.000") - try { return Timestamp.from(LocalDateTime.parse(s, isoNoTz).toInstant(ZoneOffset.UTC)); } catch (Exception ignored) {} - return null; - } - }); + // Timestamp fields (including dob) use Gson's default parsing here, same as + // on vb/stoptb. The gpsTimestamp field on RMNCH entities is parsed by + // com.iemr.common.identity.mapper.GpsTimestampAdapter via a field-level + // @JsonAdapter annotation instead of a global registration, so it can't + // affect any other Timestamp field. } }