From fc931e848eab5d23565347533fa1166a009bdd0d Mon Sep 17 00:00:00 2001 From: Vanitha S <116701245+vanitha1822@users.noreply.github.com> Date: Mon, 10 Aug 2026 16:19:50 +0530 Subject: [PATCH 1/8] fix: edit in ES (#185) --- .../elasticsearch/ElasticsearchSyncController.java | 2 +- .../iemr/common/identity/repo/BenMappingRepo.java | 6 +++++- .../BeneficiaryElasticsearchIndexUpdater.java | 14 ++++++++++++-- .../BeneficiaryTransactionHelper.java | 2 +- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/iemr/common/identity/controller/elasticsearch/ElasticsearchSyncController.java b/src/main/java/com/iemr/common/identity/controller/elasticsearch/ElasticsearchSyncController.java index 7d8f1877..7f4debc8 100644 --- a/src/main/java/com/iemr/common/identity/controller/elasticsearch/ElasticsearchSyncController.java +++ b/src/main/java/com/iemr/common/identity/controller/elasticsearch/ElasticsearchSyncController.java @@ -347,7 +347,7 @@ public ResponseEntity> checkBeneficiaryExists( try { java.math.BigInteger benRegIdBig = new java.math.BigInteger(benRegId); - boolean exists = mappingRepo.existsByBenRegId(benRegIdBig); + boolean exists = mappingRepo.countActiveByBenRegId(benRegIdBig) > 0; response.put("benRegId", benRegId); response.put("existsInDatabase", exists); diff --git a/src/main/java/com/iemr/common/identity/repo/BenMappingRepo.java b/src/main/java/com/iemr/common/identity/repo/BenMappingRepo.java index fd781922..333f893a 100644 --- a/src/main/java/com/iemr/common/identity/repo/BenMappingRepo.java +++ b/src/main/java/com/iemr/common/identity/repo/BenMappingRepo.java @@ -237,7 +237,11 @@ MBeneficiarymapping getWithVanSerialNoVanID(@Param("vanSerialNo") BigInteger van @Query("SELECT m FROM MBeneficiarymapping m WHERE m.benRegId = :benRegId AND m.deleted = false") MBeneficiarymapping findByBenRegId(@Param("benRegId") BigInteger benRegId); - /** + + @Query(value = "SELECT COUNT(*) FROM i_beneficiarymapping WHERE BenRegId = :benRegId AND Deleted = false", nativeQuery = true) + long countActiveByBenRegId(@Param("benRegId") BigInteger benRegId); + +/** * Check if beneficiary exists */ @Query(value = "SELECT COUNT(*) > 0 FROM i_beneficiarymapping WHERE BenRegId = :benRegId AND Deleted = false", nativeQuery = true) diff --git a/src/main/java/com/iemr/common/identity/service/elasticsearch/BeneficiaryElasticsearchIndexUpdater.java b/src/main/java/com/iemr/common/identity/service/elasticsearch/BeneficiaryElasticsearchIndexUpdater.java index ff2fec5b..15adc6ad 100644 --- a/src/main/java/com/iemr/common/identity/service/elasticsearch/BeneficiaryElasticsearchIndexUpdater.java +++ b/src/main/java/com/iemr/common/identity/service/elasticsearch/BeneficiaryElasticsearchIndexUpdater.java @@ -94,6 +94,16 @@ public CompletableFuture syncBeneficiaryAsync(BigInteger benRegId) { return CompletableFuture.completedFuture(null); } + // Document id must be the beneficiary id, matching the full/bulk sync and + // the delete path. Keying on benRegId here wrote a second document per + // beneficiary instead of updating the existing one, so edits never + // surfaced in search. + final String documentId = document.getBenId(); + if (documentId == null) { + logger.warn("No beneficiary id for benRegId: {}, skipping ES sync", benRegId); + return CompletableFuture.completedFuture(null); + } + // Log ABHA for verification logger.info("Syncing benRegId={} with ABHA: healthID={}, abhaID={}", benRegId, document.getHealthID(), document.getAbhaID()); @@ -102,10 +112,10 @@ public CompletableFuture syncBeneficiaryAsync(BigInteger benRegId) { // Index to ES esClient.index(i -> i .index(beneficiaryIndex) - .id(String.valueOf(benRegId)) + .id(documentId) .document(document).refresh(Refresh.True)); - logger.info("Successfully synced benRegId: {} to ES", benRegId); + logger.info("Successfully synced benRegId: {} to ES as document {}", benRegId, documentId); } catch (Exception e) { logger.error("Error syncing beneficiary {} to Elasticsearch: {}", benRegId, e.getMessage(), e); } diff --git a/src/main/java/com/iemr/common/identity/service/elasticsearch/BeneficiaryTransactionHelper.java b/src/main/java/com/iemr/common/identity/service/elasticsearch/BeneficiaryTransactionHelper.java index a173e327..5af139bf 100644 --- a/src/main/java/com/iemr/common/identity/service/elasticsearch/BeneficiaryTransactionHelper.java +++ b/src/main/java/com/iemr/common/identity/service/elasticsearch/BeneficiaryTransactionHelper.java @@ -80,7 +80,7 @@ public long countActiveBeneficiaries() { @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true, timeout = 10) public boolean existsByBenRegId(BigInteger benRegId) { try { - return mappingRepo.existsByBenRegId(benRegId); + return mappingRepo.countActiveByBenRegId(benRegId) > 0; } catch (Exception e) { logger.error("Error checking existence for benRegId={}: {}", benRegId, e.getMessage()); throw e; From cadf4d8daf1a12d947d1b4e0b3e1f298383be201 Mon Sep 17 00:00:00 2001 From: Vanitha S <116701245+vanitha1822@users.noreply.github.com> Date: Thu, 20 Aug 2026 12:00:54 +0530 Subject: [PATCH 2/8] fix: benregid null issue in i_beneficiarydetails table (#188) --- .../com/iemr/common/identity/service/IdentityService.java | 4 ++++ 1 file changed, 4 insertions(+) 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..f3e47495 100644 --- a/src/main/java/com/iemr/common/identity/service/IdentityService.java +++ b/src/main/java/com/iemr/common/identity/service/IdentityService.java @@ -1499,6 +1499,10 @@ public BeneficiaryCreateResp createIdentity(IdentityDTO identity) { // MBeneficiarydetail mDetl = mapper.identityDTOToMBeneficiarydetail(identity); MBeneficiarydetail mDetl = convertIdentityDTOToMBeneficiarydetail(identity); + if (mDetl.getBeneficiaryRegID() == null) { + mDetl.setBeneficiaryRegID(regMap.getBenRegId()); + } + if (mDetl.getCreatedDate() == null) { SimpleDateFormat sdf = new SimpleDateFormat(CREATED_DATE_FORMAT); String dateToStoreInDataBase = sdf.format(new Date()); From a87adf08f1e57a883b2de676d63d292eaf4cfd51 Mon Sep 17 00:00:00 2001 From: Vanitha S <116701245+vanitha1822@users.noreply.github.com> Date: Thu, 3 Sep 2026 10:43:19 +0530 Subject: [PATCH 3/8] fix: load beneficiary mapping associations lazily (#189) The one-to-one associations of MBeneficiarymapping were eager, so every read of a mapping pulled address, contact, consent, details, reg-id, secure stack, image and account with it. Fetching them on demand keeps the beneficiary edit path from loading the whole graph. Co-authored-by: Claude Opus 5 (1M context) --- .../identity/domain/MBeneficiarymapping.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/iemr/common/identity/domain/MBeneficiarymapping.java b/src/main/java/com/iemr/common/identity/domain/MBeneficiarymapping.java index 4397f21c..caeb3464 100644 --- a/src/main/java/com/iemr/common/identity/domain/MBeneficiarymapping.java +++ b/src/main/java/com/iemr/common/identity/domain/MBeneficiarymapping.java @@ -30,6 +30,7 @@ import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; +import jakarta.persistence.FetchType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.NamedQuery; @@ -85,7 +86,7 @@ public class MBeneficiarymapping implements Serializable { private Timestamp reservedOn; // uni-directional one-to-one association to MBeneficiaryaddress - @OneToOne + @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "BenAddressId", nullable = false) private MBeneficiaryaddress mBeneficiaryaddress; @@ -93,7 +94,7 @@ public class MBeneficiarymapping implements Serializable { private BigInteger benAddressId; // uni-directional one-to-one association to MBeneficiaryconsent - @OneToOne + @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "BenConsentId"/* , nullable=false */) private MBeneficiaryconsent mBeneficiaryconsent; @@ -101,7 +102,7 @@ public class MBeneficiarymapping implements Serializable { private BigInteger benConsentId; // uni-directional one-to-one association to MBeneficiarycontact - @OneToOne + @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "BenContactsId", nullable = false) private MBeneficiarycontact mBeneficiarycontact; @@ -109,7 +110,7 @@ public class MBeneficiarymapping implements Serializable { private BigInteger benContactsId; // uni-directional one-to-one association to MBeneficiarydetail - @OneToOne + @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "BenDetailsId", referencedColumnName = "BeneficiaryDetailsId", nullable = false) private MBeneficiarydetail mBeneficiarydetail; @@ -117,7 +118,7 @@ public class MBeneficiarymapping implements Serializable { private BigInteger benDetailsId; // uni-directional one-to-one association to MBeneficiaryregidmapping - @OneToOne + @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "BenRegId"/* , nullable=false */) private MBeneficiaryregidmapping mBeneficiaryregidmapping; @@ -126,7 +127,7 @@ public class MBeneficiarymapping implements Serializable { private BigInteger benRegId; // uni-directional one-to-one association to MBensecurestack - @OneToOne + @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "BenSecureStackId", nullable = false) private MBensecurestack mBeneficiarysecurestack; @@ -166,7 +167,7 @@ public class MBeneficiarymapping implements Serializable { * New columns added for MMU integration 11-04-2018 */ // uni-directional one-to-one association - @OneToOne + @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "BenImageId") private MBeneficiaryImage mBeneficiaryImage; @@ -174,7 +175,7 @@ public class MBeneficiarymapping implements Serializable { private BigInteger benImageId; // uni-directional one-to-one association - @OneToOne + @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "BenAccountID") private MBeneficiaryAccount mBeneficiaryAccount; From 77b38493b96effda15146106a603f622a2d9f40e Mon Sep 17 00:00:00 2001 From: Vishwanath Balkur <118195001+vishwab1@users.noreply.github.com> Date: Thu, 3 Sep 2026 15:02:40 +0530 Subject: [PATCH 4/8] Vb/release 3.9.0 merge (#190) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update pom.xml * Merge Release 3.6.2 to Main (#163) * Elasticsearch implementation for Beneficiary Search (#123) * fix: ES Implementation-mapping, indexing and async records * fix: add service for ES Search * fix: search implementation * fix: add additional fields as per the requirement * fix: comment extra fields * fix: rename the files, remove commented code * fix: update pom.xml * fix: revert advancesearch * fix: add properties * fix: coderabbit comments * fix: remove comment code * fix: accept numeric values for search * fix: update the env variable * fix: advance search functionality * fix: update the advance search ES functionality * fix: sync and fetch benid * fix: size limit issue * fix: improve response time * fix: updated the end point to advancedSearchES * fix: age issue while moving to nurse worklist (#130) * Optimize the Elasticsearch for better Response Time (#131) * fix: optimize the index and reduce the size * fix: align indent * fix: abha / health id issue * fix: sync optimization * fix: get abha created date * fix: state issue * fix: village issue * fix: add abha details * fix: abha fix and refresh index * fix: refresh index * fix: remove duplicate dependency * fix: fuzzy search, resume, refresh api's * fix: add middlename, maritalstatus * fix: remove refresh while bulk indexing * fix: add license * fix: remove the bean function to create the index automatically (#133) * Fix ES Issue in the Query (#137) * fix: ES Exception * fix: ES exception * Nd/vs/fix es (#138) * fix: ES Exception * fix: ES exception * fix: enable multi-word fuzzy search requirement (#139) * fix: enable multi-word fuzzy search requirement * Downgrade version from 3.6.2 to 3.6.1 * fix: multi-word search (#140) * Fix the column mismatch issue in beneficiary search (#142) * fix: column mismatch issue * fix: update marital status * add new column in rmnch table for death and child record * add new column in rmnch table for death and child record * Cherry-pick health and version API enhancements to release-3.6.1 (#145) * feat(health,version): add health and version endpoints * feat(health,version): add health and version endpoints without auth * fix(health): remove unused private methods * fix(health): fix exception issue * fix(health): redact error details for unauthenticated health checks * fix code quality issues and reduce cognitive complexity * feat(health): add MySQL health endpoint * refactor(health): simplify MySQL health check and remove sensitive details * fix(health): remove unused imports and variables * refactor(health): address nitpicks (configurable ES scheme, log noise, graceful shutdown, record) * fix(health): scope PROCESSLIST lock-wait check to application DB user * refactor(health): remove unused params and reuse response/error constants * fix(health): remove unused imports and methods * chore(health): clean up unused imports, params, and dead helpers * fix(health): avoid sharing JDBC connections across threads in advanced MySQL checks * refactor(health): reuse REDIS_COMPONENT constant and extract nested try block * fix(health): avoid blocking DB I/O under write lock and restore interrupt flag * fix(health): cancel in-flight futures on generic failure * feat(health,version): add index existance, read-only detection, canary write for elasticsearch health check * refactor(health): reduce cognitive complexity, remove dead throws, and clean code smells * Rebase 3.6.2 (#150) * fix: enable multi-word fuzzy search requirement (#139) * fix: enable multi-word fuzzy search requirement * Downgrade version from 3.6.2 to 3.6.1 * fix: multi-word search (#140) * Fix the column mismatch issue in beneficiary search (#142) * fix: column mismatch issue * fix: update marital status * add new column in rmnch table for death and child record * add new column in rmnch table for death and child record * Cherry-pick health and version API enhancements to release-3.6.1 (#145) * feat(health,version): add health and version endpoints * feat(health,version): add health and version endpoints without auth * fix(health): remove unused private methods * fix(health): fix exception issue * fix(health): redact error details for unauthenticated health checks * fix code quality issues and reduce cognitive complexity * feat(health): add MySQL health endpoint * refactor(health): simplify MySQL health check and remove sensitive details * fix(health): remove unused imports and variables * refactor(health): address nitpicks (configurable ES scheme, log noise, graceful shutdown, record) * fix(health): scope PROCESSLIST lock-wait check to application DB user * refactor(health): remove unused params and reuse response/error constants * fix(health): remove unused imports and methods * chore(health): clean up unused imports, params, and dead helpers * fix(health): avoid sharing JDBC connections across threads in advanced MySQL checks * refactor(health): reuse REDIS_COMPONENT constant and extract nested try block * fix(health): avoid blocking DB I/O under write lock and restore interrupt flag * fix(health): cancel in-flight futures on generic failure * feat(health,version): add index existance, read-only detection, canary write for elasticsearch health check * refactor(health): reduce cognitive complexity, remove dead throws, and clean code smells --------- Co-authored-by: Saurav Mishra Co-authored-by: Saurav Mishra <80103738+SauravBizbRolly@users.noreply.github.com> Co-authored-by: KOPPIREDDY DURGA PRASAD <144464542+DurgaPrasad-54@users.noreply.github.com> * feat(jwt): enhance jwt validation logging and public endpoint check (#151) * fix: pom version (#152) * Add the missing properties for 1097_Preprod (#153) * fix: add the missing properties * fix: update db url * docs: add CLAUDE.md for Claude Code guidance * fix: map sexualOrientationID during beneficiary update in 1097 convertIdentityEditDTOToMBeneficiarydetail() was missing sexualOrientationID and sexualOrientationType, so the field was never persisted on update. Co-Authored-By: Claude Sonnet 4.6 * Sexual orientation data not reflecting under DB record (#157) * fix: enable multi-word fuzzy search requirement (#139) * fix: enable multi-word fuzzy search requirement * Downgrade version from 3.6.2 to 3.6.1 * fix: multi-word search (#140) * Fix the column mismatch issue in beneficiary search (#142) * fix: column mismatch issue * fix: update marital status * add new column in rmnch table for death and child record * add new column in rmnch table for death and child record * Cherry-pick health and version API enhancements to release-3.6.1 (#145) * feat(health,version): add health and version endpoints * feat(health,version): add health and version endpoints without auth * fix(health): remove unused private methods * fix(health): fix exception issue * fix(health): redact error details for unauthenticated health checks * fix code quality issues and reduce cognitive complexity * feat(health): add MySQL health endpoint * refactor(health): simplify MySQL health check and remove sensitive details * fix(health): remove unused imports and variables * refactor(health): address nitpicks (configurable ES scheme, log noise, graceful shutdown, record) * fix(health): scope PROCESSLIST lock-wait check to application DB user * refactor(health): remove unused params and reuse response/error constants * fix(health): remove unused imports and methods * chore(health): clean up unused imports, params, and dead helpers * fix(health): avoid sharing JDBC connections across threads in advanced MySQL checks * refactor(health): reuse REDIS_COMPONENT constant and extract nested try block * fix(health): avoid blocking DB I/O under write lock and restore interrupt flag * fix(health): cancel in-flight futures on generic failure * feat(health,version): add index existance, read-only detection, canary write for elasticsearch health check * refactor(health): reduce cognitive complexity, remove dead throws, and clean code smells * fix: map sexualOrientationID during beneficiary update in 1097 convertIdentityEditDTOToMBeneficiarydetail() was missing sexualOrientationID and sexualOrientationType, so the field was never persisted on update. Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Vanitha S <116701245+vanitha1822@users.noreply.github.com> Co-authored-by: Saurav Mishra Co-authored-by: Saurav Mishra <80103738+SauravBizbRolly@users.noreply.github.com> Co-authored-by: KOPPIREDDY DURGA PRASAD <144464542+DurgaPrasad-54@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 * feat: add getBenFamilyDetails API and fix searchFamily queries - Add POST /family/getBenFamilyDetails endpoint that returns full family details (master record + all members) for a given beneficiaryRegId - Fix searchFamily and searchFamilyWithFamilyId queries: replace noOfmembers > 0 guard with deleted=false filter so families with zero/null member count and soft-deleted records are handled correctly; also make villageId optional and use LIKE prefix match for familyName Co-Authored-By: Claude Sonnet 4.6 * fix: preserve existing occupation and education when incoming values are null During beneficiary edit, convertIdentityEditDTOToMBeneficiarydetail creates a fresh entity and overwrites all columns on save. Added null-guards for occupationId, occupation, educationId, and education so existing DB values are preserved when the incoming DTO omits them — consistent with the existing pattern for familyId and headOfFamily_Relation. Co-Authored-By: Claude Sonnet 4.6 * fix: replace in-memory queue with SELECT FOR UPDATE SKIP LOCKED for BenRegId allocation (#159) * aam-2126 Memeberlist is not displying properly * fix: aam-2313 phone number leading with zero - removed zero (#161) * Allowing numbers with zero is search by phone number. (#162) * fix: aam-2313 phone number leading with zero - removed zero * fix: aam-2313 serach by user phone number fix for number leading with zero --------- Co-authored-by: Saurav Mishra Co-authored-by: Saurav Mishra <80103738+SauravBizbRolly@users.noreply.github.com> Co-authored-by: KOPPIREDDY DURGA PRASAD <144464542+DurgaPrasad-54@users.noreply.github.com> Co-authored-by: SnehaRH Co-authored-by: Claude Sonnet 4.6 Co-authored-by: SnehaRH <77656297+snehar-nd@users.noreply.github.com> * 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 * 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 * 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 * 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 * 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. * chore: bump version to 3.8.2 * fix duplicate ben id issue * save abha id in health maping with beneficiaryID * abha id add in entity class * abha id add in entity class * abha id add in entity class * abha id add in entity class * abha id add in entity class * abha id add in entity class * abha id add in entity class * abha id add in entity class * 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. * 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. * 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 * 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 * 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 * 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. * chore: bump version to 3.8.2 * duplicate ben id issue * Add server urls and update swagger workflow (#135) * docs(readme): add documentation badge * chore(swagger): automate swagger sync to amrit-docs * chore(swagger): automate swagger sync to amrit-docs * chore(swagger): update github workflow for swagger json * fix(swagger): fix the stop api issue in swagger workflow * fix(swagger): removed the unnecessary initial sleep * chore(swagger): add env-driven Dev/UAT/Demo servers and update swagger workflow * 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. * 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 * abha id add in entity class * fix: fail open on Redis errors in token denylist check A transient Redis connection blip during isTokenDenylisted() was thrown as an exception and treated as authentication failure, causing valid, unexpired JWTs to be intermittently rejected with 401 during normal API calls (e.g. beneficiary registration). Co-Authored-By: Claude Sonnet 4.6 * Revert "fix: fail open on Redis errors in token denylist check" This reverts commit 03ad7fec3b17098acbad6a6dab7b70ddd2b3053a. * 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 * fixed issue of Query did not return a unique result: 2 results were returned in RMNCH * fixed issue of Query did not return a unique result: 2 results were returned in RMNCH * 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 * attach logger in response * fix duplicate issue * 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 * Tuned DB connection pools and enable response compression (#172) * chore: add trace logging to /id/create for registerNew debugging Prints the raw request body, parsed IdentityDTO, and svc.createIdentity result so a Linux-server trace can show whether the beneficiary is actually being saved. Co-Authored-By: Claude Sonnet 5 * chore: add trace logging to syncDataToAmrit Prints request body and response so the AMRIT sync path can be traced alongside the registerNew flow. Co-Authored-By: Claude Sonnet 5 * Fix the Elasticsearch Health Status (#173) * fix: authenticate elasticsearch health probes against secured cluster The /health Elasticsearch client was built without credentials, so probes against a security-enabled cluster returned 401 and reported ES DOWN (forcing overall status DOWN) even though ES was healthy. Inject elasticsearch.username/ password and attach a BasicCredentialsProvider, matching ElasticsearchConfig. Auth is skipped when username is blank (ES security disabled). Co-Authored-By: Claude Opus 4.8 (1M context) * fix: pom version --------- Co-authored-by: Claude Opus 4.8 (1M context) * chore(dob): add debug trace logging at DTO->entity mapping and around detailRepo.save Co-Authored-By: Claude Sonnet 5 * feat(household): add totalHhMembers and registeredAtCampSite fields Co-Authored-By: Claude Sonnet 5 * Adding placeOfCurrentLiving, otherPlaceOfCurrentLiving and institutionName in RMNCHMBeneficiarydetail and RMNCHBeneficiaryDetailsRmnch (#175) * chore(trace): log pinCode presence in i_bendemographics during syncDataToAmrit Co-Authored-By: Claude Sonnet 5 * Revert "Tuned DB connection pools and enable response compression (#172)" This reverts commit c0f6dea7854e030b11512ce7ba204e0303718b93. * 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 * fix(household): accept Pincode key variant during RMNCH sync deserialization FLW-Mobile-App's Household.kt serializes the field as "Pincode" (capital P), but the entity had no @SerializedName so Gson's exact-case field matching against "pincode" silently dropped the value on every sync. Co-Authored-By: Claude Sonnet 5 * feat(household): add address column mapping to i_householddetails New address VARCHAR(500) column added to i_householddetails so household address can be captured once at HH Registration instead of being duplicated per-member on i_beneficiaryaddress. Accepts both "address" and "Address" keys during sync, following the same pattern as the earlier pincode key-case bug. Co-Authored-By: Claude Sonnet 5 * Merge 3.8.2 code (#177) * 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 * 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 * 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 * 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 * 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. * 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. * 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 * 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 * 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 * 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 * 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 --------- Co-authored-by: Claude Sonnet 4.6 Co-authored-by: Sehjot Singh Pannu * feat(household): add totalHhMembers and registeredAtCampSite fields Co-Authored-By: Claude Sonnet 5 * feat(rmnch): add placeOfCurrentLiving, otherPlaceOfCurrentLiving, institutionName fields * fix(household): accept Pincode key variant during RMNCH sync deserialization FLW-Mobile-App's Household.kt serializes the field as "Pincode" (capital P), but the entity had no @SerializedName so Gson's exact-case field matching against "pincode" silently dropped the value on every sync. Co-Authored-By: Claude Sonnet 5 * feat(household): add address column mapping to i_householddetails New address VARCHAR(500) column added to i_householddetails so household address can be captured once at HH Registration instead of being duplicated per-member on i_beneficiaryaddress. Accepts both "address" and "Address" keys during sync, following the same pattern as the earlier pincode key-case bug. Co-Authored-By: Claude Sonnet 5 * chore(release): bump version to 3.8.3 * fix(rmnch): stop garbage device timestamps from corrupting CreatedDate i_beneficiarydetails_rmnch.CreatedDate was trusting whatever the syncing device sent, with no validation. Confirmed on UAT: 48/463 rows future-dated (up to 2026-08-29, ~3 weeks ahead), 6/463 stuck at the 1970 epoch default — both classic broken-device-clock symptoms (never set, or set ahead). Add isPlausibleDeviceTimestamp() (rejects null, epoch-default, and future-dated values) and apply it in syncDataToAmrit()'s RMNCH beneficiary details block: - Existing record already has a plausible CreatedDate -> keep it; a later re-sync must never overwrite a good value with a possibly-bad one. - Existing record's value is bad but this sync's incoming value is plausible -> self-heal using the incoming value. - Neither is plausible (broken clock both times) -> fall back to server sync time as the least-wrong value instead of storing garbage. - New record with a plausible device value -> trust it as-is, since offline captures legitimately sync well after the actual event. Build verified: mvn -o clean compile, BUILD SUCCESS. Co-Authored-By: Claude Sonnet 5 * fix(stoptb): enforce vanID in createIdentity(), require explicit enforce.vanid config - IdentityService.createIdentity(): previously had zero enforce-vanid check at all (only RmnchDataSyncServiceImpl had it) - now throws if vanID is missing and enforceVanID=true, matching the convention everywhere else - Remove inline :false default from stoptb.enforce.vanid @Value in both IdentityService and RmnchDataSyncServiceImpl - every properties file must set this explicitly now, so a forgotten config fails loudly at startup instead of running fail-open - Add stoptb.enforce.vanid=false to the 1097_* profile properties (separate Maven build sharing these same classes, not a van deployment, so stays false, but must be explicit) Co-Authored-By: Claude Sonnet 5 * fix(stoptb): fix RMNCH VanID placeholder-zero and id/VanSerialNo collision - RMNCHBeneficiaryDetailsRmnch: mobile sends VanID=0 as a placeholder (not null) for a fresh record - the existing '== null' check never caught it, leaving the real vanID from Redis unapplied. Now checks for both null and 0. - Both RMNCHBeneficiaryDetailsRmnch and RMNCHHouseHoldDetails map their VanSerialNo column to a Java field literally named 'id' with no @SerializedName - any incoming JSON payload that happens to carry its own 'id' key collides with it during Gson deserialization, silently overwriting the intended VanSerialNo with whatever unrelated value the client sent (observed live: every row stuck at VanSerialNo=1). Added updateVanSerialNo() to both repos and call it after each saveAll(), same pattern already used for i_beneficiaryimage/i_beneficiaryaddress in IdentityService.createIdentity(). - Left RMNCHCBACdetails/RMNCHBornBirthDetails alone despite sharing the same id-collision pattern - not part of the Stop TB flow, zero rows in practice, out of scope for this fix. Co-Authored-By: Claude Sonnet 5 * fix(stoptb): read vanID from stoptb.van.id property instead of Redis Redis (camp:vanID) was written once at MMU login and deleted globally, unscoped, on ANY user's logout — a Redis outage or an unrelated user's logout would silently break RMNCH sync on this camp. Each camp/van already runs its own dedicated backend instance, so which van this is never actually changes at runtime. syncDataToAmrit() now reads vanID from the new stoptb.van.id property (no inline default, every properties file must set it explicitly, same convention as stoptb.enforce.vanid) instead of an inline Redis lookup via RedisStorage. Removed the now-unused RedisStorage dependency from this class. Added stoptb.van.id=0 to the 1097 (Helpline) profile's properties files too, since RmnchDataSyncServiceImpl is shared with that build and the property now has no inline default. Scope: vanID only, parkingPlaceID is not part of this change. Co-Authored-By: Claude Sonnet 5 * Removing duplicate fields in i_beneficiaryaddress table (#186) Co-authored-by: Saurav Mishra <80103738+SauravBizbRolly@users.noreply.github.com> --------- Co-authored-by: Amoghavarsh <93114621+5Amogh@users.noreply.github.com> Co-authored-by: Vanitha S <116701245+vanitha1822@users.noreply.github.com> Co-authored-by: Saurav Mishra Co-authored-by: Saurav Mishra <80103738+SauravBizbRolly@users.noreply.github.com> Co-authored-by: KOPPIREDDY DURGA PRASAD <144464542+DurgaPrasad-54@users.noreply.github.com> Co-authored-by: SnehaRH Co-authored-by: Claude Sonnet 4.6 Co-authored-by: SnehaRH <77656297+snehar-nd@users.noreply.github.com> Co-authored-by: Sehjot Singh Pannu --- pom.xml | 2 +- src/main/environment/1097_ci.properties | 8 + src/main/environment/1097_docker.properties | 8 + src/main/environment/1097_example.properties | 8 + src/main/environment/common_ci.properties | 5 + src/main/environment/common_docker.properties | 5 + .../environment/common_example.properties | 7 + .../controller/IdentityController.java | 7 +- .../rmnch/RMNCHMobileAppController.java | 9 +- .../rmnch/RMNCHBeneficiaryDetailsRmnch.java | 62 +++ .../data/rmnch/RMNCHHouseHoldDetails.java | 90 +++-- .../data/rmnch/RMNCHMBeneficiarydetail.java | 17 + .../iemr/common/identity/domain/Address.java | 12 + .../identity/domain/MBeneficiaryaddress.java | 42 +-- .../identity/mapper/GpsTimestampAdapter.java | 116 ++++++ .../identity/mapper/IdentityMapper.java | 78 ++-- .../common/identity/mapper/InputMapper.java | 4 + .../common/identity/repo/BenDetailRepo.java | 49 +-- .../RMNCHBeneficiaryDetailsRmnchRepo.java | 11 + .../repo/rmnch/RMNCHBornBirthDetailsRepo.java | 3 +- .../repo/rmnch/RMNCHCBACDetailsRepo.java | 2 +- .../repo/rmnch/RMNCHHouseHoldDetailsRepo.java | 11 + .../identity/service/IdentityService.java | 278 +++++++------- .../service/health/HealthService.java | 37 +- .../service/rmnch/RmnchDataSyncService.java | 2 +- .../rmnch/RmnchDataSyncServiceImpl.java | 352 +++++++++++++++++- .../utils/JwtUserIdValidationFilter.java | 2 +- .../identity/utils/mapper/InputMapper.java | 5 + .../identity/utils/redis/RedisStorage.java | 7 + src/main/resources/application.properties | 1 + 30 files changed, 972 insertions(+), 268 deletions(-) create mode 100644 src/main/java/com/iemr/common/identity/mapper/GpsTimestampAdapter.java diff --git a/pom.xml b/pom.xml index 9eda8258..04fad9b0 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.iemr.common.identity identity-api - 3.9.0 + 3.8.3 war diff --git a/src/main/environment/1097_ci.properties b/src/main/environment/1097_ci.properties index 514f675d..371df704 100644 --- a/src/main/environment/1097_ci.properties +++ b/src/main/environment/1097_ci.properties @@ -34,3 +34,11 @@ elasticsearch.index.beneficiary=@env.ELASTICSEARCH_INDEX_BENEFICIARY@ # Enable/Disable ES (for gradual rollout) elasticsearch.enabled=@env.ELASTICSEARCH_ENABLED@ +# Van/local-laptop deployments only. The 1097 service is not a van deployment, so this +# stays false — but must still be set explicitly, since IdentityService.java (shared with +# the common_* build) now requires this property with no inline default. +stoptb.enforce.vanid=false +# Same reasoning — not a van deployment, but RmnchDataSyncServiceImpl (shared with the +# common_* build) now requires this property explicitly, no inline default. +stoptb.van.id=0 + diff --git a/src/main/environment/1097_docker.properties b/src/main/environment/1097_docker.properties index 88df0354..d3eefc0d 100644 --- a/src/main/environment/1097_docker.properties +++ b/src/main/environment/1097_docker.properties @@ -34,3 +34,11 @@ elasticsearch.index.beneficiary=${ELASTICSEARCH_INDEX_BENEFICIARY} # Enable/Disable ES (for gradual rollout) elasticsearch.enabled=${ELASTICSEARCH_ENABLED} +# Van/local-laptop deployments only. The 1097 service is not a van deployment, so this +# stays false — but must still be set explicitly, since IdentityService.java (shared with +# the common_* build) now requires this property with no inline default. +stoptb.enforce.vanid=false +# Same reasoning — not a van deployment, but RmnchDataSyncServiceImpl (shared with the +# common_* build) now requires this property explicitly, no inline default. +stoptb.van.id=0 + diff --git a/src/main/environment/1097_example.properties b/src/main/environment/1097_example.properties index ba2f3211..ab928d52 100644 --- a/src/main/environment/1097_example.properties +++ b/src/main/environment/1097_example.properties @@ -31,3 +31,11 @@ elasticsearch.index.beneficiary=beneficiary_index # Enable/Disable ES (for gradual rollout) elasticsearch.enabled=true +# Van/local-laptop deployments only. The 1097 service is not a van deployment, so this +# stays false — but must still be set explicitly, since IdentityService.java (shared with +# the common_* build) now requires this property with no inline default. +stoptb.enforce.vanid=false +# Same reasoning — not a van deployment, but RmnchDataSyncServiceImpl (shared with the +# common_* build) now requires this property explicitly, no inline default. +stoptb.van.id=0 + diff --git a/src/main/environment/common_ci.properties b/src/main/environment/common_ci.properties index e32dc366..ae899db2 100644 --- a/src/main/environment/common_ci.properties +++ b/src/main/environment/common_ci.properties @@ -22,6 +22,11 @@ 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@ +# Stop TB: this deployment's van/camp ID, replacing the old Redis camp:vanID lookup +stoptb.van.id=@env.STOPTB_VAN_ID@ + 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..3eed5222 100644 --- a/src/main/environment/common_docker.properties +++ b/src/main/environment/common_docker.properties @@ -22,6 +22,11 @@ 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} +# Stop TB: this deployment's van/camp ID, replacing the old Redis camp:vanID lookup +stoptb.van.id=${STOPTB_VAN_ID} + 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..b9d36842 100644 --- a/src/main/environment/common_example.properties +++ b/src/main/environment/common_example.properties @@ -17,6 +17,13 @@ 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 +# Stop TB: this deployment's van/camp ID, replacing the old Redis camp:vanID lookup +stoptb.van.id=0 + cors.allowed-origins=http://localhost:* # Elasticsearch Configuration 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..2482ac47 100644 --- a/src/main/java/com/iemr/common/identity/controller/IdentityController.java +++ b/src/main/java/com/iemr/common/identity/controller/IdentityController.java @@ -607,7 +607,12 @@ public String createIdentity(@Param(value = "{\r\n" + " \"eventTypeName\": \"St + " \"sexualOrientationType\": \"String\",\r\n" + " \"vanID\": \"Integer\",\r\n" + " \"createdDate\": \"Timestamp\"\r\n" + " \"faceEmbedding\": [\"Float\"]\r\n" + "}") @RequestBody String identityData) throws IEMRException { logger.info("IdentityController.createIdentity - start"); - + + // 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; diff --git a/src/main/java/com/iemr/common/identity/controller/rmnch/RMNCHMobileAppController.java b/src/main/java/com/iemr/common/identity/controller/rmnch/RMNCHMobileAppController.java index cbb91cce..deada381 100644 --- a/src/main/java/com/iemr/common/identity/controller/rmnch/RMNCHMobileAppController.java +++ b/src/main/java/com/iemr/common/identity/controller/rmnch/RMNCHMobileAppController.java @@ -59,12 +59,17 @@ public class RMNCHMobileAppController { @PostMapping(value = "/syncDataToAmrit", consumes = "application/json", produces = "application/json") @Operation(summary = "Sync data to AMRIT for already regestered beneficiary with AMRIT beneficiary id ") - public String syncDataToAmrit(@RequestBody String requestOBJ) { + public String syncDataToAmrit(@RequestBody String requestOBJ,@RequestHeader(value = "jwttoken") String authorization) { OutputResponse response = new OutputResponse(); try { if (requestOBJ != null) { - String s = rmnchDataSyncService.syncDataToAmrit(requestOBJ); + + String s = rmnchDataSyncService.syncDataToAmrit(requestOBJ,authorization); + logger.info("syncDataToAmrit Response: {}", s); + response.setResponse(s); + + logger.info(" syncDataToAmrit Final API Response: {}", response.toString()); } else response.setError(5000, "Invalid/NULL request obj"); } catch (Exception e) { 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 4cab210c..a6604f69 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; @@ -390,6 +392,9 @@ public class RMNCHBeneficiaryDetailsRmnch { @Expose @Transient private String addressLine3; + @Expose + @Transient + private String pinCode; // ---------------------------------------------- @@ -527,6 +532,15 @@ public class RMNCHBeneficiaryDetailsRmnch { @Expose private String otherPlaceOfDeath; + @Expose + private String placeOfCurrentLiving; + + @Expose + private String otherPlaceOfCurrentLiving; + + @Expose + private String institutionName; + @Expose private Boolean isSpouseAdded; @@ -550,4 +564,52 @@ public class RMNCHBeneficiaryDetailsRmnch { @Expose private Boolean isDeactivate; + @Expose + @Transient + private String abhaId; + + @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 + + @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") + @JsonAdapter(GpsTimestampAdapter.class) + 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..471bdaa6 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,14 @@ 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; /** - * + * * @author de40034072 * */ @@ -111,6 +114,23 @@ public class RMNCHHouseHoldDetails { @Column(name = "familyName") private String familyName; + @Expose + @SerializedName(value = "address", alternate = "Address") + @Column(name = "address") + private String address; + + @Expose + @Column(name = "totalHhMembers") + private Integer totalHhMembers; + + @Expose + @Column(name = "registeredAtCampSite") + private String registeredAtCampSite; + + @Expose + @Column(name = "registeredAtCampSiteId") + private Integer registeredAtCampSiteId; + @Expose @Column(name = "fuelUsed") private String fuelUsed; @@ -191,6 +211,7 @@ public class RMNCHHouseHoldDetails { private String other_sourceofDrinkingWater; @Expose + @SerializedName(value = "pincode", alternate = "Pincode") @Column(name = "pincode") private Integer pincode; @@ -359,4 +380,31 @@ 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") + @JsonAdapter(GpsTimestampAdapter.class) + 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/RMNCHMBeneficiarydetail.java b/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHMBeneficiarydetail.java index 397c5a66..0e86127e 100644 --- a/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHMBeneficiarydetail.java +++ b/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHMBeneficiarydetail.java @@ -214,4 +214,21 @@ public class RMNCHMBeneficiarydetail { @Expose @Transient private Integer ProviderServiceMapID; + + @Expose + @Transient + private String abhaId; + + @Expose + @Column(name = "familyid") + private String familyId; + + @Expose + private String placeOfCurrentLiving; + + @Expose + private String otherPlaceOfCurrentLiving; + + @Expose + private String institutionName; } \ No newline at end of file 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..f355cbc8 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,11 @@ import lombok.Data; +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; @@ -51,4 +56,11 @@ private Integer vanID; private Integer parkingPlaceID; + 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/domain/MBeneficiaryaddress.java b/src/main/java/com/iemr/common/identity/domain/MBeneficiaryaddress.java index 0e2ab415..3c98e412 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") 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/IdentityMapper.java b/src/main/java/com/iemr/common/identity/mapper/IdentityMapper.java index bc3ad686..1e2b1920 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); @@ -296,13 +296,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 +334,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 +481,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 +490,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/mapper/InputMapper.java b/src/main/java/com/iemr/common/identity/mapper/InputMapper.java index 9a46ba4f..3549efc2 100644 --- a/src/main/java/com/iemr/common/identity/mapper/InputMapper.java +++ b/src/main/java/com/iemr/common/identity/mapper/InputMapper.java @@ -44,6 +44,10 @@ public class InputMapper 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); 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..07fc0638 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 */ @@ -155,7 +160,7 @@ int untagFamily(@Param("modifiedBy") String modifiedBy, @Param("vanSerialNo") Bi "m.BenRegId, " + // 0 "brm.beneficiaryID, " + // 1 "d.FirstName, " + // 2 - "d.MiddleName, " + // 3 + "d.MiddleName, " + // 3 "d.LastName, " + // 4 "d.GenderID, " + // 5 "g.GenderName, " + // 6 @@ -163,7 +168,7 @@ int untagFamily(@Param("modifiedBy") String modifiedBy, @Param("vanSerialNo") Bi "TIMESTAMPDIFF(YEAR, d.DOB, CURDATE()) as Age, " + // 8 "d.FatherName, " + // 9 "d.SpouseName, " + // 10 - "d.MaritalStatusID, " + // 11 + "d.MaritalStatusID, " + // 11 "ms.Status as MaritalStatusName, " + // 12 "d.IsHIVPositive, " + // 13 "m.CreatedBy, " + // 14 @@ -181,19 +186,19 @@ int untagFamily(@Param("modifiedBy") String modifiedBy, @Param("vanSerialNo") Bi "addr.CurrServicePoint, " + // 26 "addr.ParkingPlaceID, " + // 27 "contact.PreferredPhoneNum, " + // 28 - "addr.CurrVillageId, " + // 29 - "addr.CurrVillage " + // 30 + "addr.CurrVillageId, " + // 29 + "addr.CurrVillage " + // 30 "FROM i_beneficiarymapping m " + "LEFT JOIN i_beneficiarydetails d ON m.BenDetailsId = d.BeneficiaryDetailsID " + "LEFT JOIN m_beneficiaryregidmapping brm ON brm.BenRegId = m.BenRegId " + "LEFT JOIN db_iemr.m_gender g ON d.GenderID = g.GenderID " + - "LEFT JOIN db_iemr.m_maritalstatus ms ON d.MaritalStatusID = ms.StatusID " + + "LEFT JOIN db_iemr.m_maritalstatus ms ON d.MaritalStatusID = ms.StatusID " + "LEFT JOIN i_beneficiaryaddress addr ON m.BenAddressId = addr.BenAddressID " + "LEFT JOIN i_beneficiarycontacts contact ON m.BenContactsId = contact.BenContactsID " + - "WHERE m.BenRegId IN (:ids) AND m.Deleted = false", + "WHERE m.BenRegId IN (:ids) AND m.Deleted = false", nativeQuery = true) List findCompleteDataByIds(@Param("ids") List ids); - + /** * Direct search in database (fallback) */ @@ -201,7 +206,7 @@ int untagFamily(@Param("modifiedBy") String modifiedBy, @Param("vanSerialNo") Bi "m.BenRegId, " + "brm.beneficiaryID, " + "d.FirstName, " + - "d.MiddleName, " + + "d.MiddleName, " + "d.LastName, " + "d.GenderID, " + "g.GenderName, " + @@ -209,8 +214,8 @@ int untagFamily(@Param("modifiedBy") String modifiedBy, @Param("vanSerialNo") Bi "TIMESTAMPDIFF(YEAR, d.DOB, CURDATE()) as Age, " + "d.FatherName, " + "d.SpouseName, " + - "d.MaritalStatusID, " + - "ms.Status as MaritalStatusName, " + + "d.MaritalStatusID, " + + "ms.Status as MaritalStatusName, " + "d.IsHIVPositive, " + "m.CreatedBy, " + "m.CreatedDate, " + @@ -227,17 +232,17 @@ int untagFamily(@Param("modifiedBy") String modifiedBy, @Param("vanSerialNo") Bi "addr.CurrServicePoint, " + "addr.ParkingPlaceID, " + "contact.PreferredPhoneNum, " + - "addr.CurrVillageId, " + - "addr.CurrVillage " + + "addr.CurrVillageId, " + + "addr.CurrVillage " + "FROM i_beneficiarymapping m " + "LEFT JOIN i_beneficiarydetails d ON m.BenDetailsId = d.BeneficiaryDetailsID " + "LEFT JOIN db_iemr.m_gender g ON d.GenderID = g.GenderID " + "LEFT JOIN m_beneficiaryregidmapping brm ON brm.BenRegId = m.BenRegId " + - "LEFT JOIN db_iemr.m_maritalstatus ms ON d.MaritalStatusID = ms.StatusID " + + "LEFT JOIN db_iemr.m_maritalstatus ms ON d.MaritalStatusID = ms.StatusID " + "LEFT JOIN i_beneficiaryaddress addr ON m.BenAddressId = addr.BenAddressID " + "LEFT JOIN i_beneficiarycontacts contact ON m.BenContactsId = contact.BenContactsID " + "WHERE (d.FirstName LIKE CONCAT('%', :query, '%') " + - " OR d.MiddleName LIKE CONCAT('%', :query, '%') " + + " OR d.MiddleName LIKE CONCAT('%', :query, '%') " + " OR d.LastName LIKE CONCAT('%', :query, '%') " + " OR d.FatherName LIKE CONCAT('%', :query, '%') " + " OR d.BeneficiaryRegID = :query " + @@ -248,10 +253,10 @@ int untagFamily(@Param("modifiedBy") String modifiedBy, @Param("vanSerialNo") Bi " OR contact.PhoneNum4 = :query " + " OR contact.PhoneNum5 = :query) " + "AND m.Deleted = false " + - "LIMIT 20", + "LIMIT 20", nativeQuery = true) List searchBeneficiaries(@Param("query") String query); - + /** * Get all phone numbers for a beneficiary */ @@ -287,10 +292,10 @@ int untagFamily(@Param("modifiedBy") String modifiedBy, @Param("vanSerialNo") Bi "FROM i_beneficiarymapping m " + "LEFT JOIN i_beneficiarycontacts contact ON m.BenContactsId = contact.BenContactsID " + "WHERE m.BenRegId = :beneficiaryId AND contact.PhoneNum5 IS NOT NULL " + - "ORDER BY priority", + "ORDER BY priority", nativeQuery = true) List findPhoneNumbersByBeneficiaryId(@Param("beneficiaryId") Long beneficiaryId); - + // Advance Search ES @Query(value = "SELECT DISTINCT " + @@ -323,14 +328,14 @@ int untagFamily(@Param("modifiedBy") String modifiedBy, @Param("vanSerialNo") Bi "addr.CurrServicePoint, " + // 26 "addr.ParkingPlaceID, " + // 27 "contact.PreferredPhoneNum, " + // 28 - "addr.CurrVillageId, " + - "addr.CurrVillage " + + "addr.CurrVillageId, " + + "addr.CurrVillage " + "FROM i_beneficiarymapping m " + "LEFT JOIN i_beneficiarydetails d " + " ON m.BenDetailsId = d.BeneficiaryDetailsID " + "LEFT JOIN db_iemr.m_gender g " + " ON d.GenderID = g.GenderID " + - "LEFT JOIN db_iemr.m_maritalstatus ms ON d.MaritalStatusID = ms.StatusID " + + "LEFT JOIN db_iemr.m_maritalstatus ms ON d.MaritalStatusID = ms.StatusID " + "LEFT JOIN i_beneficiaryaddress addr " + " ON m.BenAddressId = addr.BenAddressID " + "LEFT JOIN i_beneficiarycontacts contact " + @@ -339,7 +344,7 @@ int untagFamily(@Param("modifiedBy") String modifiedBy, @Param("vanSerialNo") Bi "WHERE m.Deleted = false " + "AND (:firstName IS NULL OR d.FirstName LIKE CONCAT('%', :firstName, '%')) " + - "AND (:middleName IS NULL OR d.MiddleName LIKE CONCAT('%', :middleName, '%')) " + + "AND (:middleName IS NULL OR d.MiddleName LIKE CONCAT('%', :middleName, '%')) " + "AND (:lastName IS NULL OR d.LastName LIKE CONCAT('%', :lastName, '%')) " + "AND (:genderId IS NULL OR d.GenderID = :genderId) " + "AND (:dob IS NULL OR DATE(d.DOB) = DATE(:dob)) " + diff --git a/src/main/java/com/iemr/common/identity/repo/rmnch/RMNCHBeneficiaryDetailsRmnchRepo.java b/src/main/java/com/iemr/common/identity/repo/rmnch/RMNCHBeneficiaryDetailsRmnchRepo.java index 52916f31..cc36e5b4 100644 --- a/src/main/java/com/iemr/common/identity/repo/rmnch/RMNCHBeneficiaryDetailsRmnchRepo.java +++ b/src/main/java/com/iemr/common/identity/repo/rmnch/RMNCHBeneficiaryDetailsRmnchRepo.java @@ -24,10 +24,12 @@ import java.math.BigInteger; import java.util.List; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; import com.iemr.common.identity.data.rmnch.RMNCHBeneficiaryDetailsRmnch; @@ -39,4 +41,13 @@ public RMNCHBeneficiaryDetailsRmnch getByIdAndVanID(@Param("vanSerialNo") BigInt @Query(" SELECT t FROM RMNCHBeneficiaryDetailsRmnch t WHERE t.BenRegId =:benRegID ") public List getByRegID(@Param("benRegID") BigInteger benRegId); + + // The Java field bound to the VanSerialNo column is literally named `id` with no + // @SerializedName - any incoming JSON that happens to carry its own "id" key (e.g. a + // client-side list-item id) collides with it during Gson deserialization and silently + // overwrites the intended VanSerialNo value. Force it back to the row's own PK after save. + @Transactional + @Modifying + @Query("UPDATE RMNCHBeneficiaryDetailsRmnch t SET t.id = :id WHERE t.beneficiaryDetails_RmnchId = :id") + void updateVanSerialNo(@Param("id") BigInteger id); } diff --git a/src/main/java/com/iemr/common/identity/repo/rmnch/RMNCHBornBirthDetailsRepo.java b/src/main/java/com/iemr/common/identity/repo/rmnch/RMNCHBornBirthDetailsRepo.java index 2af4d5b7..21488700 100644 --- a/src/main/java/com/iemr/common/identity/repo/rmnch/RMNCHBornBirthDetailsRepo.java +++ b/src/main/java/com/iemr/common/identity/repo/rmnch/RMNCHBornBirthDetailsRepo.java @@ -22,6 +22,7 @@ package com.iemr.common.identity.repo.rmnch; import java.math.BigInteger; +import java.util.List; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; @@ -36,5 +37,5 @@ public interface RMNCHBornBirthDetailsRepo extends CrudRepository getByRegID(@Param("benRegID") BigInteger benRegID); } diff --git a/src/main/java/com/iemr/common/identity/repo/rmnch/RMNCHCBACDetailsRepo.java b/src/main/java/com/iemr/common/identity/repo/rmnch/RMNCHCBACDetailsRepo.java index 49fb4697..95e452b4 100644 --- a/src/main/java/com/iemr/common/identity/repo/rmnch/RMNCHCBACDetailsRepo.java +++ b/src/main/java/com/iemr/common/identity/repo/rmnch/RMNCHCBACDetailsRepo.java @@ -37,7 +37,7 @@ public interface RMNCHCBACDetailsRepo extends CrudRepository getByRegID(@Param("benRegID") BigInteger benRegID); @Query(value = "select beneficiary_visit_code,visit_category from db_iemr.i_ben_flow_outreach where beneficiary_reg_id=:benRegID AND beneficiary_visit_code is not null AND visit_category is not null order by created_date desc limit 1", nativeQuery = true) public List getVisitDetailsbyRegID(@Param("benRegID") Long benRegID); diff --git a/src/main/java/com/iemr/common/identity/repo/rmnch/RMNCHHouseHoldDetailsRepo.java b/src/main/java/com/iemr/common/identity/repo/rmnch/RMNCHHouseHoldDetailsRepo.java index 76490cbd..b8e0fc4b 100644 --- a/src/main/java/com/iemr/common/identity/repo/rmnch/RMNCHHouseHoldDetailsRepo.java +++ b/src/main/java/com/iemr/common/identity/repo/rmnch/RMNCHHouseHoldDetailsRepo.java @@ -21,10 +21,12 @@ */ package com.iemr.common.identity.repo.rmnch; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; import com.iemr.common.identity.data.rmnch.RMNCHHouseHoldDetails; @@ -37,4 +39,13 @@ public interface RMNCHHouseHoldDetailsRepo extends CrudRepository getByHouseHoldID(@Param("houseoldId") long houseoldId); + + // The Java field bound to the VanSerialNo column is literally named `id` with no + // @SerializedName - any incoming JSON that happens to carry its own "id" key collides + // with it during Gson deserialization and silently overwrites the intended VanSerialNo + // value. Force it back to the row's own PK after save. + @Transactional + @Modifying + @Query("UPDATE RMNCHHouseHoldDetails t SET t.id = :id WHERE t.houseHoldDetailsId = :id") + void updateVanSerialNo(@Param("id") Long id); } 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 f3e47495..0d965dd5 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; @@ -169,6 +169,14 @@ private JdbcTemplate getJdbcTemplate() { @Value("${elasticsearch.enabled}") private boolean esEnabled; + // Van/local-laptop deployments only — see RmnchDataSyncServiceImpl and FLW-API's + // CampConfigService for the same flag. createIdentity() previously had no enforcement + // check at all, so a missing vanID here would silently save VanID=NULL instead of failing. + // No inline default: every properties file must set this explicitly, so a forgotten + // config fails loudly at startup instead of silently running fail-open. + @Value("${stoptb.enforce.vanid}") + private boolean enforceVanID; + public void getBenAdress() { logger.debug("Address count: " + addressRepo.count()); logger.debug( @@ -229,7 +237,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 +249,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 +294,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 +306,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 +523,7 @@ public List getBeneficiariesByBenId(BigInteger benId) /** * - * @param BenRegId + * @param benRegId * @return */ public List getBeneficiariesByBenRegId(BigInteger benRegId) @@ -591,104 +599,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 +771,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 +797,7 @@ public List searhBeneficiaryByFamilyId(String familyId) } public List searchBeneficiaryByVillageIdAndLastModifyDate(List villageIDs, - Timestamp lastModifiedDate) { + Timestamp lastModifiedDate) { List beneficiaryList = new ArrayList<>(); try { @@ -843,7 +851,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( @@ -912,9 +920,6 @@ private MBeneficiarymapping getBeneficiariesDTONew(Object[] benMapArr) { } } - - - } return mapping; } @@ -1124,7 +1129,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 +1156,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 +1274,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 +1299,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 +1319,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(); @@ -1388,6 +1393,11 @@ private MBeneficiarydetail convertIdentityEditDTOToMBeneficiarydetail(IdentityEd public BeneficiaryCreateResp createIdentity(IdentityDTO identity) { logger.info("IdentityService.createIdentity - start"); + if (identity.getVanID() == null && enforceVanID) { + throw new IllegalStateException( + "Camp not configured: vanID missing. Please select van/service point in MMU before registering beneficiary."); + } + // Atomically claim the next available ID using SELECT … FOR UPDATE SKIP LOCKED. // This is safe across multiple app servers sharing the same database — each server // locks and reserves a distinct row, so duplicate BenRegId inserts cannot occur. @@ -1771,8 +1781,8 @@ private MBeneficiaryImage identityDTOToMBeneficiaryImage(IdentityDTO identity) { beneficiaryImage.setCreatedDate(identity.getCreatedDate()); if (identity.getVanID() != null) { beneficiaryImage.setVanID(identity.getVanID()); - } - if (identity.getBenFamilyDTOs() != null) { + } else if (identity.getBenFamilyDTOs() != null && !identity.getBenFamilyDTOs().isEmpty() + && identity.getBenFamilyDTOs().get(0).getVanID() != null) { beneficiaryImage.setVanID(identity.getBenFamilyDTOs().get(0).getVanID()); } @@ -1943,7 +1953,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) { @@ -2161,7 +2171,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); diff --git a/src/main/java/com/iemr/common/identity/service/health/HealthService.java b/src/main/java/com/iemr/common/identity/service/health/HealthService.java index f233d729..a8385b11 100644 --- a/src/main/java/com/iemr/common/identity/service/health/HealthService.java +++ b/src/main/java/com/iemr/common/identity/service/health/HealthService.java @@ -48,10 +48,14 @@ import javax.management.ObjectName; import jakarta.annotation.PostConstruct; import org.apache.http.HttpHost; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.config.RequestConfig; +import org.apache.http.impl.client.BasicCredentialsProvider; import org.elasticsearch.client.Request; import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.RestClient; +import org.elasticsearch.client.RestClientBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -116,6 +120,8 @@ public class HealthService { private final boolean elasticsearchEnabled; private final boolean elasticsearchIndexingRequired; private final String elasticsearchTargetIndex; + private final String elasticsearchUsername; + private final String elasticsearchPassword; private static final ObjectMapper objectMapper = new ObjectMapper(); private RestClient elasticsearchRestClient; @@ -139,7 +145,9 @@ public HealthService( @Value("${elasticsearch.port:9200}") int elasticsearchPort, @Value("${elasticsearch.enabled:false}") boolean elasticsearchEnabled, @Value("${elasticsearch.target-index:amrit_data}") String elasticsearchTargetIndex, - @Value("${elasticsearch.indexing-required:false}") boolean elasticsearchIndexingRequired) { + @Value("${elasticsearch.indexing-required:false}") boolean elasticsearchIndexingRequired, + @Value("${elasticsearch.username:}") String elasticsearchUsername, + @Value("${elasticsearch.password:}") String elasticsearchPassword) { this.dataSource = dataSource; this.advancedCheckExecutor = Executors.newSingleThreadExecutor(r -> { @@ -153,6 +161,8 @@ public HealthService( this.elasticsearchEnabled = elasticsearchEnabled; this.elasticsearchIndexingRequired = elasticsearchIndexingRequired; this.elasticsearchTargetIndex = (elasticsearchTargetIndex != null) ? elasticsearchTargetIndex : "amrit_data"; + this.elasticsearchUsername = elasticsearchUsername; + this.elasticsearchPassword = elasticsearchPassword; } @PostConstruct @@ -176,15 +186,30 @@ public void cleanup() { private void initializeElasticsearchClient() { try { - this.elasticsearchRestClient = RestClient.builder( + RestClientBuilder builder = RestClient.builder( new HttpHost(elasticsearchHost, elasticsearchPort, "http")) .setRequestConfigCallback(cb -> cb .setConnectTimeout(ELASTICSEARCH_CONNECT_TIMEOUT_MS) - .setSocketTimeout(ELASTICSEARCH_SOCKET_TIMEOUT_MS)) - .build(); + .setSocketTimeout(ELASTICSEARCH_SOCKET_TIMEOUT_MS)); + + // Attach Basic Auth when credentials are configured, so the health + // probes authenticate against a security-enabled cluster (matches + // ElasticsearchConfig). When username is blank (security disabled), + // the client stays unauthenticated. + if (elasticsearchUsername != null && !elasticsearchUsername.isEmpty()) { + BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials( + AuthScope.ANY, + new UsernamePasswordCredentials(elasticsearchUsername, elasticsearchPassword)); + builder.setHttpClientConfigCallback(httpClientBuilder -> + httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider)); + } + + this.elasticsearchRestClient = builder.build(); this.elasticsearchClientReady = true; - logger.info("Elasticsearch client initialized (connect/socket timeout: {}ms)", - ELASTICSEARCH_CONNECT_TIMEOUT_MS); + logger.info("Elasticsearch client initialized (connect/socket timeout: {}ms, auth: {})", + ELASTICSEARCH_CONNECT_TIMEOUT_MS, + (elasticsearchUsername != null && !elasticsearchUsername.isEmpty()) ? "enabled" : "disabled"); } catch (Exception e) { logger.warn("Failed to initialize Elasticsearch client: {}", e.getMessage()); this.elasticsearchClientReady = false; diff --git a/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncService.java b/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncService.java index 845e79f9..7bbcb316 100644 --- a/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncService.java +++ b/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncService.java @@ -22,7 +22,7 @@ package com.iemr.common.identity.service.rmnch; public interface RmnchDataSyncService { - public String syncDataToAmrit(String requestOBJ) throws Exception; + public String syncDataToAmrit(String requestOBJ, String authorization) throws Exception; public String saveBeneficiaryDetailsAfterRegistration( Long beneficiaryID, Long beneficiaryRegID, 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 ca85397f..aec3c44a 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 @@ -24,6 +24,7 @@ import java.math.BigInteger; import java.sql.Date; import java.sql.Timestamp; +import java.text.SimpleDateFormat; import java.time.Period; import java.util.ArrayList; import java.util.Arrays; @@ -33,8 +34,6 @@ import java.util.Map; import java.util.regex.Pattern; -import com.iemr.common.identity.utils.OutputResponse; -import io.swagger.v3.oas.annotations.Operation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -42,10 +41,12 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; +import org.springframework.http.*; import org.springframework.stereotype.Service; 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; @@ -73,13 +74,15 @@ 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; import com.iemr.common.identity.utils.http.HttpUtils; import com.iemr.common.identity.utils.mapper.InputMapper; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; @Service @Qualifier("rmnchServiceImpl") @@ -113,9 +116,31 @@ public class RmnchDataSyncServiceImpl implements RmnchDataSyncService { private RMNCHBenContactRepo rMNCHBenContactRepo; @Autowired private RMNCHMBenRegIdMapRepo rMNCHMBenRegIdMapRepo; + @Autowired + private BenDetailRepo benDetailRepo; + + @Value("${fhir-url}") + private String fhirUrl; + + // This deployment's van/camp ID. Previously looked up from Redis ("camp:vanID"), + // written at MMU login and deleted (globally, unscoped) on ANY user's logout — a Redis + // outage or an unrelated user's logout would silently break sync on this camp. Each + // camp/van already runs its own dedicated backend instance, so which van this is never + // actually changes at runtime; reading it from properties removes the Redis dependency + // entirely. No inline default — every properties file must set this explicitly. + // Scope: vanID only, parkingPlaceID is not part of this change. + @Value("${stoptb.van.id}") + private int configuredVanID; + + // When true, sync fails loudly if camp is not configured instead of silently + // skipping vanID stamping. No inline default — every properties file must set this + // explicitly, so a forgotten config fails loudly at startup instead of running fail-open. + @Value("${stoptb.enforce.vanid}") + private boolean enforceVanID; @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) @Override - public String syncDataToAmrit(String requestOBJ) throws Exception { + public String syncDataToAmrit(String requestOBJ, String authorization) throws Exception { + Map resultMap = new HashMap(); @@ -124,6 +149,17 @@ public String syncDataToAmrit(String requestOBJ) throws Exception { ArrayList cBACDetailsIds = new ArrayList<>(); ArrayList houseHoldDetailsIds = new ArrayList<>(); + // Configured van ID for this deployment (see configuredVanID field javadoc above). + // parkingPlaceID is out of scope for this change — kept null, same as before whenever + // Redis had no value for it. + Integer campVanID = configuredVanID > 0 ? configuredVanID : null; + if (campVanID == null && enforceVanID) { + throw new Exception( + "Camp not configured: stoptb.van.id is 0. Set stoptb.van.id in this deployment's properties file."); + } + final Integer vanID = campVanID; + final Integer parkingPlaceID = null; + try { if (requestOBJ != null && !requestOBJ.isEmpty()) { JsonObject jsnOBJ = new JsonObject(); @@ -133,6 +169,8 @@ public String syncDataToAmrit(String requestOBJ) throws Exception { // other tables data saving // ben details RMNCH extra fields details + logger.info("Request object of syncDataToAmrit: "+jsnOBJ); + BigInteger benRegID = null; @@ -145,18 +183,63 @@ public String syncDataToAmrit(String requestOBJ) throws Exception { // 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 .getByRegID(benRegID).get(0); if (temp != null) { obj.setBeneficiaryDetails_RmnchId(temp.getBeneficiaryDetails_RmnchId()); + if (isPlausibleDeviceTimestamp(temp.getCreatedDate())) { + // Already has a good CreatedDate from the first sync — a later + // re-sync must never overwrite it. + obj.setCreatedDate(temp.getCreatedDate()); + } else if (isPlausibleDeviceTimestamp(obj.getCreatedDate())) { + // Stored value was garbage but this re-sync brought a plausible + // one from the device — self-heal using it (obj already has it). + } else { + obj.setCreatedDate(new Timestamp(System.currentTimeMillis())); + } } + } else if (!isPlausibleDeviceTimestamp(obj.getCreatedDate())) { + // Device clock is broken (unset -> 1970 epoch, or set ahead -> future + // date). We can't recover the true capture time, so fall back to the + // sync time as the least-wrong value instead of storing garbage. + obj.setCreatedDate(new Timestamp(System.currentTimeMillis())); } + // else: trust the device-supplied CreatedDate as-is — offline captures + // legitimately sync well after the actual event, so a later server time + // would be less accurate than the device's own timestamp. @@ -172,6 +255,12 @@ public String syncDataToAmrit(String requestOBJ) throws Exception { } obj.setRelatedBeneficiaryIdsDB(sb.toString()); } + // Mobile sends VanID=0 as a placeholder (not null) for a fresh record — + // `== null` alone never catches it, leaving the placeholder in place. + if ((obj.getVanID() == null || obj.getVanID() == 0) && vanID != null) { + obj.setVanID(vanID); + obj.setParkingPlaceID(parkingPlaceID); + } if(!rMNCHBenDetailsRepo.getByBenRegID(obj.getBenRegId()).isEmpty()){ RMNCHMBeneficiarydetail rmnchmBeneficiarydetail = rMNCHBenDetailsRepo.getByBenRegID(obj.getBenRegId()).get(0); @@ -186,20 +275,55 @@ public String syncDataToAmrit(String requestOBJ) throws Exception { rmnchmBeneficiarydetail.setGenderId(obj.getGenderId()); rmnchmBeneficiarydetail.setMaritalstatus(obj.getMaritalstatus()); rmnchmBeneficiarydetail.setMaritalstatusId(obj.getMaritalstatusId()); + rmnchmBeneficiarydetail.setPlaceOfCurrentLiving(obj.getPlaceOfCurrentLiving()); + rmnchmBeneficiarydetail.setOtherPlaceOfCurrentLiving(obj.getOtherPlaceOfCurrentLiving()); + rmnchmBeneficiarydetail.setInstitutionName(obj.getInstitutionName()); + if(obj.getFamilyId()!=null && !obj.getFamilyId().isEmpty()){ + rmnchmBeneficiarydetail.setFamilyId(obj.getFamilyId()); + + } benDetailsList.add(rmnchmBeneficiarydetail); + if (obj.getAbhaId()!=null && !obj.getAbhaId().isEmpty()) { + mapHealthIDToBeneficiary(authorization,obj.getBenRegId().longValue(),obj.getBenficieryid().longValue(),obj.getAbhaId(),obj.getCreatedBy(),obj.getFirstName(),obj.getLastName(),obj.getDob().toString(),obj.getProviderServiceMapID()); + + } + } } } + // 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); + // The `id`/VanSerialNo field is Gson-collision-prone (see repo javadoc) — + // force it back to each row's own PK after save. + benDetailsExtraList.forEach((n) -> rMNCHBeneficiaryDetailsRmnchRepo + .updateVanSerialNo(n.getBeneficiaryDetails_RmnchId())); benDetailsExtraList.forEach((n) -> beneficiaryDetailsIds.add(n.getId())); // 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 : benDetailsOriginalList) { + 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() @@ -208,9 +332,15 @@ public String syncDataToAmrit(String requestOBJ) throws Exception { for (RMNCHBornBirthDetails obj : bornBirthList) { benRegID = rMNCHMBenRegIdMapRepo.getRegID(obj.getBenficieryid()); obj.setBenRegId(benRegID); - RMNCHBornBirthDetails temp = rMNCHBornBirthDetailsRepo.getByRegID(benRegID); - if (temp != null) - obj.setBornBirthDeatilsId(temp.getBornBirthDeatilsId()); + if(!rMNCHBornBirthDetailsRepo.getByRegID(benRegID).isEmpty()){ + RMNCHBornBirthDetails temp = rMNCHBornBirthDetailsRepo.getByRegID(benRegID).get(0); + if (temp != null) + obj.setBornBirthDeatilsId(temp.getBornBirthDeatilsId()); + } + if (obj.getVanID() == null && vanID != null) { + obj.setVanID(vanID); + obj.setParkingPlaceID(parkingPlaceID); + } } bornBirthList = (ArrayList) rMNCHBornBirthDetailsRepo .saveAll(bornBirthList); @@ -231,9 +361,15 @@ public String syncDataToAmrit(String requestOBJ) throws Exception { obj.setConfirmed_tb("Not checked"); obj.setConfirmed_ncd_diseases("Not checked"); obj.setDiagnosis_status("pending"); - RMNCHCBACdetails temp = rMNCHCBACDetailsRepo.getByRegID(benRegID); - if (temp != null) - obj.setCBACDetailsid(temp.getCBACDetailsid()); + if(!rMNCHCBACDetailsRepo.getByRegID(benRegID).isEmpty()){ + RMNCHCBACdetails temp = rMNCHCBACDetailsRepo.getByRegID(benRegID).get(0); + if (temp != null) + obj.setCBACDetailsid(temp.getCBACDetailsid()); + } + if (obj.getVanID() == null && vanID != null) { + obj.setVanID(vanID); + obj.setParkingPlaceID(parkingPlaceID); + } } cbacList = (ArrayList) rMNCHCBACDetailsRepo.saveAll(cbacList); @@ -246,6 +382,22 @@ public String syncDataToAmrit(String requestOBJ) throws Exception { .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()){ @@ -253,11 +405,24 @@ public String syncDataToAmrit(String requestOBJ) throws Exception { .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()))); + } + // Set VanID/ParkingPlaceID for both NEW and existing households — this must + // stay OUTSIDE the "household already exists" block above (it's regressed + // back inside there twice already via merges), otherwise a brand-new + // household never gets VanID stamped, breaking van-scoped sync. + if (obj.getVanID() == null && vanID != null) { + obj.setVanID(vanID); + obj.setParkingPlaceID(parkingPlaceID); } - } houseHoldList = (ArrayList) rMNCHHouseHoldDetailsRepo .saveAll(houseHoldList); + // The `id`/VanSerialNo field is Gson-collision-prone (see repo javadoc) — + // force it back to each row's own PK after save. + houseHoldList.forEach((n) -> rMNCHHouseHoldDetailsRepo + .updateVanSerialNo(n.getHouseHoldDetailsId())); // success response houseHoldList.forEach((n) -> houseHoldDetailsIds.add(n.getId())); } @@ -285,6 +450,110 @@ public String syncDataToAmrit(String requestOBJ) throws Exception { return new Gson().toJson(resultMap); } + /** + * Splits a list into sub-lists (batches) of the given size. + * Last batch may contain fewer elements. + */ + private List> partitionList(List list, int batchSize) { + List> batches = new ArrayList<>(); + if (list == null || list.isEmpty()) { + return batches; + } + for (int i = 0; i < list.size(); i += batchSize) { + batches.add(new ArrayList<>(list.subList(i, Math.min(i + batchSize, list.size())))); + } + return batches; + } + + public String mapHealthIDToBeneficiary(String authorization, + Long benRegID, + Long beneficiaryID, + String abhaId, + String createdBy,String firstName,String lastName,String dob,Integer providerServiceMapId) { + try { + RestTemplate restTemplate = new RestTemplate(); + String formattedDob = dob; + + try { + if (dob != null && dob.contains(" ")) { + Timestamp timestamp = Timestamp.valueOf(dob); + formattedDob = new SimpleDateFormat("dd-MM-yyyy") + .format(timestamp); + } + } catch (Exception ex) { + logger.warn("DOB format conversion failed, sending original DOB : {}", dob); + } + logger.info("Authorization Token : {}", authorization); + + Map requestMap = new HashMap<>(); + + requestMap.put("beneficiaryRegID", benRegID); + requestMap.put("beneficiaryID", beneficiaryID); + requestMap.put("healthIdNumber", abhaId); + + requestMap.put("createdBy", createdBy); + requestMap.put("providerServiceMapId", providerServiceMapId); + requestMap.put("isNew", false); + + // ABHA Profile + Map abhaProfile = new HashMap<>(); + abhaProfile.put("ABHANumber", abhaId); + + List phrAddress = new ArrayList<>(); + phrAddress.add(abhaId + "@abdm"); + + abhaProfile.put("phrAddress", phrAddress); + abhaProfile.put("firstName", firstName); + abhaProfile.put("middleName", ""); + abhaProfile.put("lastName", lastName); + abhaProfile.put("dob", formattedDob); + + + requestMap.put("ABHAProfile", abhaProfile); + + String requestBody = new Gson().toJson(requestMap); + + String url = fhirUrl + + ConfigProperties.getPropertyByName("mapHealthIDToBeneficiary"); + + logger.info("Calling URL : {}", url); + logger.info("Request Body : {}", requestBody); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + headers.set("Jwttoken", authorization); + + HttpEntity entity = + new HttpEntity<>(requestBody, headers); + + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.POST, + entity, + String.class + ); + + logger.info("ABHA Mapping Response : {}", response.getBody()); + + return response.getBody(); + + } catch (HttpClientErrorException e) { + + logger.error("HTTP Error Status : {}", e.getStatusCode()); + logger.error("HTTP Error Response : {}", e.getResponseBodyAsString(), e); + + return "HTTP Error : " + e.getStatusCode(); + + } catch (Exception e) { + + logger.error("Error while saving Health ID Mapping", e); + + return "Error Save Health Id : " + e.getMessage(); + } + + } + @Override @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) @@ -418,6 +687,44 @@ private Integer getInt(JsonObject obj, String key, Integer defaultVal) { ? obj.get(key).getAsInt() : defaultVal; } + + /** + * A device-supplied CreatedDate is trustworthy only if it is non-null, + * not the classic unset-clock epoch default, and not after the current + * server time (an offline capture can only have happened before the sync + * request that reports it, so a future date means the device's clock is + * wrong, not that the record is legitimately from the future). + */ + private boolean isPlausibleDeviceTimestamp(Timestamp createdDate) { + if (createdDate == null) { + return false; + } + long epochDayZero = 24L * 60 * 60 * 1000; // guard band around 1970-01-01 for epoch defaults + long now = System.currentTimeMillis(); + return createdDate.getTime() > epochDayZero && createdDate.getTime() <= now; + } + + 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; @@ -562,10 +869,15 @@ private String getMappingsForAddressIDs(List addressLi benDetailsRMNCHOBJ = rMNCHBeneficiaryDetailsRmnchRepo .getByRegID(m.getBenRegId()).get(0); } + if(!rMNCHBornBirthDetailsRepo.getByRegID(m.getBenRegId()).isEmpty()){ + benBotnBirthRMNCHROBJ = rMNCHBornBirthDetailsRepo.getByRegID(m.getBenRegId()).get(0); + + } + if(! rMNCHCBACDetailsRepo.getByRegID(m.getBenRegId()).isEmpty()){ + benCABCRMNCHROBJ = rMNCHCBACDetailsRepo.getByRegID(m.getBenRegId()).get(0); - benBotnBirthRMNCHROBJ = rMNCHBornBirthDetailsRepo.getByRegID(m.getBenRegId()); + } - benCABCRMNCHROBJ = rMNCHCBACDetailsRepo.getByRegID(m.getBenRegId()); // 20-09-2021,start NcdTbHrpData res = getHRP_NCD_TB_SuspectedStatus(m.getBenRegId().longValue(), authorisation, benDetailsOBJ); @@ -586,8 +898,12 @@ private String getMappingsForAddressIDs(List addressLi // 20-09-2021,end if (benDetailsRMNCHOBJ != null && benDetailsRMNCHOBJ.getHouseoldId() != null) - benHouseHoldRMNCHROBJ = rMNCHHouseHoldDetailsRepo - .getByHouseHoldID(benDetailsRMNCHOBJ.getHouseoldId()).get(0); + if(!rMNCHHouseHoldDetailsRepo + .getByHouseHoldID(benDetailsRMNCHOBJ.getHouseoldId()).isEmpty()){ + benHouseHoldRMNCHROBJ = rMNCHHouseHoldDetailsRepo + .getByHouseHoldID(benDetailsRMNCHOBJ.getHouseoldId()).get(0); + } + } if (benDetailsRMNCHOBJ == null) @@ -655,6 +971,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) { diff --git a/src/main/java/com/iemr/common/identity/utils/JwtUserIdValidationFilter.java b/src/main/java/com/iemr/common/identity/utils/JwtUserIdValidationFilter.java index cf959aa6..cf0fd63c 100644 --- a/src/main/java/com/iemr/common/identity/utils/JwtUserIdValidationFilter.java +++ b/src/main/java/com/iemr/common/identity/utils/JwtUserIdValidationFilter.java @@ -48,7 +48,7 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo logger.info("JwtUserIdValidationFilter invoked for requestURI: {}, servletPath: {}", path, servletPath); // Skip JWT validation for public endpoints - if (servletPath.equals("/health") || servletPath.equals("/version") || + if (servletPath.equals("/health") || servletPath.equals("/version") || path.endsWith("/health") || path.endsWith("/version")) { logger.info("Public endpoint accessed: {} - skipping JWT validation", path); filterChain.doFilter(servletRequest, servletResponse); 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..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 @@ -48,6 +48,11 @@ public InputMapper() { if (builder == null) { builder = new GsonBuilder(); builder.setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"); + // 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. } } 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); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 12be1b44..79ba181f 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -148,6 +148,7 @@ spring.jpa.properties.hibernate.show_sql=false door-to-door-page-size=2 get-HRP-Status=ANC/getHRPStatus getHealthID=healthID/getBenhealthID +mapHealthIDToBeneficiary=healthIDRecord/mapHealthIDToBeneficiary spring.main.allow-bean-definition-overriding=true spring.main.allow-circular-references=true From 33abfc87dc9172f86cb7f5f51c9584eb415a8c00 Mon Sep 17 00:00:00 2001 From: Vanitha S <116701245+vanitha1822@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:46:17 +0530 Subject: [PATCH 5/8] fix: edit in ES (#193) From d4078d15172eef89833791f7bc2d9bf9c980b954 Mon Sep 17 00:00:00 2001 From: Vishwanath Balkur <118195001+vishwab1@users.noreply.github.com> Date: Fri, 11 Sep 2026 12:27:11 +0530 Subject: [PATCH 6/8] Vb/release 3.9.0 fix (#195) * fix: sync beneficiary occupation/community/religion/pincode/phone on edit (STOP-444) RmnchDataSyncServiceImpl's edit-sync path (syncDataToAmrit) only ever wrote a fixed 13-field whitelist to i_beneficiarydetails when updating an existing beneficiary (name/dob/gender/maritalstatus/family fields). occupation, community, religion, pincode, and phone number were silently dropped on every edit, regardless of what the app actually sent. Root cause: these fields are either sent nested inside i_bendemographics (only ever read for GPS before this fix), in a benPhoneMaps[] array, or under lowercase JSON keys (e.g. "villageid") that don't case-match the Java field names (e.g. villageId) under Gson's default case-sensitive binding. Traced and verified against real captured edit payloads, not assumed field names. Changes: - RMNCHBeneficiaryDetailsRmnch: add occupation/occupationId/phoneNo fields (previously had no field to bind these into at all). - RMNCHMBenMappingRepo: add getByBenRegId(), needed to resolve a beneficiary's address/contact rows for the edit. - RmnchDataSyncServiceImpl: - extract occupation/community/pinCode/blockID from i_bendemographics, phoneNo from benPhoneMaps[0], and villageid/villagename directly (case-mismatch workaround) during the existing GPS-extraction pass - set community/occupation/religion onto the updated detail entity - new updateAddressAndContactOnEdit(): resolves the beneficiary's address/contact rows via i_beneficiarymapping and updates pinCode, block (via the already-correctly-bound facilitySelection), district, village, and phone -- only for fields the incoming payload actually sent, so an edit that only changes name/occupation can't blank out address data it didn't include Deliberately NOT fixed here: economicStatus. The app sends it, but no persisted column for it exists on the beneficiary record in this schema -- it appears to belong on the household record (i_householddetails.type_bpl_apl) instead. Persisting it onto the beneficiary would be guessing at the wrong data model without confirmation. Verified live end-to-end against a real edit (benRegId 45387): occupation, community, pincode, block, village, and phone all confirmed landing correctly in the database. Household edit has a separate, more severe bug (full-object overwrite silently wipes street/colony/village on any edit) -- out of scope for this commit, tracked separately. Co-Authored-By: Claude Sonnet 5 * chore: bump version to 3.9.0 pom.xml still said 3.8.3 on this branch (cut from release-3.9.0) -- version string was never bumped when the branch was created. Co-Authored-By: Claude Sonnet 5 --------- Co-authored-by: Claude Sonnet 5 --- pom.xml | 2 +- .../rmnch/RMNCHBeneficiaryDetailsRmnch.java | 12 ++ .../repo/rmnch/RMNCHMBenMappingRepo.java | 6 + .../rmnch/RmnchDataSyncServiceImpl.java | 156 +++++++++++++++++- 4 files changed, 170 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 04fad9b0..9eda8258 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.iemr.common.identity identity-api - 3.8.3 + 3.9.0 war 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 a6604f69..60805dcf 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 @@ -408,6 +408,18 @@ public class RMNCHBeneficiaryDetailsRmnch { @Expose @Transient private Integer communityId; + // Added for STOP-444 followup: edit sync silently dropped these — mobile sends them + // (see STOP-TB-App BenSending.kt) but this entity had no field to bind them to, so + // Gson/Jackson discarded them before they ever reached RmnchDataSyncServiceImpl. + @Expose + @Transient + private String occupation; + @Expose + @Transient + private Integer occupationId; + @Expose + @Transient + private String phoneNo; @Expose @Transient private String contact_number; diff --git a/src/main/java/com/iemr/common/identity/repo/rmnch/RMNCHMBenMappingRepo.java b/src/main/java/com/iemr/common/identity/repo/rmnch/RMNCHMBenMappingRepo.java index 18415e85..5827f9f1 100644 --- a/src/main/java/com/iemr/common/identity/repo/rmnch/RMNCHMBenMappingRepo.java +++ b/src/main/java/com/iemr/common/identity/repo/rmnch/RMNCHMBenMappingRepo.java @@ -40,6 +40,12 @@ public RMNCHMBeneficiarymapping getByIdAndVanID(@Param("vanSerialNo") BigInteger public RMNCHMBeneficiarymapping getByAddressIDAndVanID(@Param("addressID") BigInteger addressID, @Param("vanID") int vanID); + // Added for STOP-444 followup: resolve a beneficiary's address/contact IDs by BenRegId + // so an edit sync can update the existing address/contact rows instead of only + // i_beneficiarydetails. + @Query(" SELECT t FROM RMNCHMBeneficiarymapping t WHERE t.benRegId = :benRegId") + public RMNCHMBeneficiarymapping getByBenRegId(@Param("benRegId") BigInteger benRegId); + @Query(nativeQuery = true, value = " SELECT userid FROM db_iemr.m_user WHERE UserName = :userName ") public Integer getUserIDByUserName(@Param("userName") String userName); 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 aec3c44a..3a2674aa 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 @@ -186,14 +186,23 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex // Build GPS lookup map from i_bendemographics in raw JSON Map benGpsMap = new HashMap<>(); + // STOP-444 followup fix: also keep the raw top-level object per beneficiary — + // occupation/community/pinCode live inside i_bendemographics (only ever read + // for GPS before this fix), and phone number lives in benPhoneMaps[0], neither + // of which Gson could bind onto RMNCHBeneficiaryDetailsRmnch: the JSON here uses + // lowercase keys (e.g. "villageid") that don't case-match the Java fields, and + // i_bendemographics/benPhoneMaps are nested, not flat. + Map benRawMap = 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")); + if (benJson.has("benficieryid") && !benJson.get("benficieryid").isJsonNull()) { + benRawMap.put(benJson.get("benficieryid").getAsBigInteger(), benJson); + if (benJson.has("i_bendemographics") + && !benJson.get("i_bendemographics").isJsonNull()) { + benGpsMap.put(benJson.get("benficieryid").getAsBigInteger(), + benJson.getAsJsonObject("i_bendemographics")); + } } } @@ -213,6 +222,41 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex obj.setGpsTimestamp(new Timestamp(demog.get("gpsTimestamp").getAsLong())); if (demog.has("isGpsUnavailable") && !demog.get("isGpsUnavailable").isJsonNull()) obj.setIsGpsUnavailable(demog.get("isGpsUnavailable").getAsBoolean()); + // STOP-444 followup fix: same source object, previously never read for + // anything but GPS. economicStatus deliberately NOT extracted here — its + // correct persisted home is unconfirmed (looks like it belongs on the + // household record, i_householddetails.type_bpl_apl, not the beneficiary). + if (demog.has("occupation") && !demog.get("occupation").isJsonNull()) + obj.setOccupation(demog.get("occupation").getAsString()); + if (demog.has("communityName") && !demog.get("communityName").isJsonNull()) + obj.setCommunity(demog.get("communityName").getAsString()); + if (demog.has("communityID") && !demog.get("communityID").isJsonNull()) + obj.setCommunityId(demog.get("communityID").getAsInt()); + if (demog.has("pinCode") && !demog.get("pinCode").isJsonNull()) + obj.setPinCode(demog.get("pinCode").getAsString()); + if (demog.has("blockID") && !demog.get("blockID").isJsonNull()) + obj.setBlockId(demog.get("blockID").getAsInt()); + } + // STOP-444 followup fix: phone number is sent as benPhoneMaps[0].phoneNo, not + // a flat field — same lowercase-key mismatch problem as village/block below. + JsonObject benJsonForPhone = benRawMap.get(obj.getBenficieryid()); + if (benJsonForPhone != null && benJsonForPhone.has("benPhoneMaps") + && !benJsonForPhone.get("benPhoneMaps").isJsonNull()) { + JsonArray phoneMaps = benJsonForPhone.getAsJsonArray("benPhoneMaps"); + if (phoneMaps.size() > 0) { + JsonObject firstPhone = phoneMaps.get(0).getAsJsonObject(); + if (firstPhone.has("phoneNo") && !firstPhone.get("phoneNo").isJsonNull()) { + obj.setPhoneNo(firstPhone.get("phoneNo").getAsString()); + } + } + // STOP-444 followup fix: entity fields are villageId/villageName (camelCase) + // but the app sends lowercase "villageid"/"villagename" — Gson's default + // matching is case-sensitive, so these never bound at all. districtid/ + // districtname already happen to case-match and don't need this treatment. + if (benJsonForPhone.has("villageid") && !benJsonForPhone.get("villageid").isJsonNull()) + obj.setVillageId(benJsonForPhone.get("villageid").getAsInt()); + if (benJsonForPhone.has("villagename") && !benJsonForPhone.get("villagename").isJsonNull()) + obj.setVillageName(benJsonForPhone.get("villagename").getAsString()); } if(!rMNCHBeneficiaryDetailsRmnchRepo .getByRegID(benRegID).isEmpty()){ @@ -278,6 +322,26 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex rmnchmBeneficiarydetail.setPlaceOfCurrentLiving(obj.getPlaceOfCurrentLiving()); rmnchmBeneficiarydetail.setOtherPlaceOfCurrentLiving(obj.getOtherPlaceOfCurrentLiving()); rmnchmBeneficiarydetail.setInstitutionName(obj.getInstitutionName()); + // STOP-444 followup fix: these were previously never re-synced on an edit of an + // existing beneficiary, so any change to them from the app was silently dropped. + if (obj.getCommunity() != null) { + rmnchmBeneficiarydetail.setCommunity(obj.getCommunity()); + } + if (obj.getCommunityId() != null) { + rmnchmBeneficiarydetail.setCommunityId(obj.getCommunityId()); + } + if (obj.getOccupation() != null) { + rmnchmBeneficiarydetail.setOccupation(obj.getOccupation()); + } + if (obj.getOccupationId() != null) { + rmnchmBeneficiarydetail.setOccupationId(obj.getOccupationId()); + } + if (obj.getReligion() != null) { + rmnchmBeneficiarydetail.setReligion(obj.getReligion()); + } + if (obj.getReligionID() != null) { + rmnchmBeneficiarydetail.setReligionID(obj.getReligionID()); + } if(obj.getFamilyId()!=null && !obj.getFamilyId().isEmpty()){ rmnchmBeneficiarydetail.setFamilyId(obj.getFamilyId()); @@ -288,6 +352,17 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex } + // STOP-444 followup fix: pinCode/block/district/village/phone live on the + // address and contact tables, which this sync never touched on edit at all + // (only ever written at initial creation). Best-effort — only overwrites a + // field when the app actually sent a non-null value for it. + try { + updateAddressAndContactOnEdit(obj); + } catch (Exception ex) { + logger.warn("Failed to update address/contact on edit for benRegId: " + + obj.getBenRegId() + " - " + ex.getMessage()); + } + } } @@ -704,6 +779,77 @@ private boolean isPlausibleDeviceTimestamp(Timestamp createdDate) { return createdDate.getTime() > epochDayZero && createdDate.getTime() <= now; } + // STOP-444 followup fix: address (pinCode/block/district/village) and phone number + // live on i_beneficiaryaddress / i_beneficiarycontacts, which the edit-sync path never + // wrote to at all before this — only the initial create path (createIdentity) ever + // touched them. Best-effort: resolves the beneficiary's address/contact rows via + // i_beneficiarymapping and updates only the fields the app actually sent a value for, + // so an edit that only changes name/occupation doesn't blank out address fields the + // app didn't include in that particular payload. + private void updateAddressAndContactOnEdit(RMNCHBeneficiaryDetailsRmnch obj) { + if (obj.getBenRegId() == null || obj.getVanID() == null) { + return; + } + RMNCHMBeneficiarymapping mapping = rMNCHMBenMappingRepo.getByBenRegId(obj.getBenRegId()); + if (mapping == null) { + return; + } + int vanID = obj.getVanID(); + + if (mapping.getBenAddressId() != null) { + RMNCHMBeneficiaryaddress address = rMNCHBenAddressRepo.getByIdAndVanID(mapping.getBenAddressId(), vanID); + if (address != null) { + boolean changed = false; + if (obj.getPinCode() != null) { + address.setPermPinCode(obj.getPinCode()); + changed = true; + } + if (obj.getBlockId() != null) { + address.setPermSubDistrictId(obj.getBlockId()); + changed = true; + } + // No block NAME source verified in the real edit payload — demog only carries + // blockID (a number). facilitySelection is a real, correctly-bound column and is + // used as the block name elsewhere in this codebase for the same reason (see + // BenRepo.kt on the mobile side), so it's the one confirmed-correct source here. + if (obj.getFacilitySelection() != null) { + address.setPermSubDistrict(obj.getFacilitySelection()); + changed = true; + } + if (obj.getDistrictid() != null) { + address.setDistrictidPerm(obj.getDistrictid()); + changed = true; + } + if (obj.getDistrictname() != null) { + address.setDistrictnamePerm(obj.getDistrictname()); + changed = true; + } + if (obj.getVillageId() != null) { + address.setVillageidPerm(obj.getVillageId()); + changed = true; + } + if (obj.getVillageName() != null) { + address.setVillagenamePerm(obj.getVillageName()); + changed = true; + } + // zoneID / servicePointID / servicePointName dropped from this fix — no confirmed + // source key for these in a real captured edit payload, and guessing a wrong Gson + // binding here would look fixed while silently still doing nothing. + if (changed) { + rMNCHBenAddressRepo.save(address); + } + } + } + + if (mapping.getBenContactsId() != null && obj.getPhoneNo() != null) { + RMNCHMBeneficiarycontact contact = rMNCHBenContactRepo.getByIdAndVanID(mapping.getBenContactsId(), vanID); + if (contact != null) { + contact.setPreferredPhoneNum(obj.getPhoneNo()); + rMNCHBenContactRepo.save(contact); + } + } + } + private boolean hasAnthropometryData(RMNCHBeneficiaryDetailsRmnch obj) { return obj.getHeight() != null || obj.getWeight() != null || obj.getBmi() != null || obj.getTemperature() != null; From acbd7a6afd4bd129d99b5c99e46680c41671f11a Mon Sep 17 00:00:00 2001 From: Vishwanath Balkur <118195001+vishwab1@users.noreply.github.com> Date: Fri, 11 Sep 2026 14:26:40 +0530 Subject: [PATCH 7/8] Fix edit fields for benficiary (#196) * fix: sync beneficiary occupation/community/religion/pincode/phone on edit (STOP-444) RmnchDataSyncServiceImpl's edit-sync path (syncDataToAmrit) only ever wrote a fixed 13-field whitelist to i_beneficiarydetails when updating an existing beneficiary (name/dob/gender/maritalstatus/family fields). occupation, community, religion, pincode, and phone number were silently dropped on every edit, regardless of what the app actually sent. Root cause: these fields are either sent nested inside i_bendemographics (only ever read for GPS before this fix), in a benPhoneMaps[] array, or under lowercase JSON keys (e.g. "villageid") that don't case-match the Java field names (e.g. villageId) under Gson's default case-sensitive binding. Traced and verified against real captured edit payloads, not assumed field names. Changes: - RMNCHBeneficiaryDetailsRmnch: add occupation/occupationId/phoneNo fields (previously had no field to bind these into at all). - RMNCHMBenMappingRepo: add getByBenRegId(), needed to resolve a beneficiary's address/contact rows for the edit. - RmnchDataSyncServiceImpl: - extract occupation/community/pinCode/blockID from i_bendemographics, phoneNo from benPhoneMaps[0], and villageid/villagename directly (case-mismatch workaround) during the existing GPS-extraction pass - set community/occupation/religion onto the updated detail entity - new updateAddressAndContactOnEdit(): resolves the beneficiary's address/contact rows via i_beneficiarymapping and updates pinCode, block (via the already-correctly-bound facilitySelection), district, village, and phone -- only for fields the incoming payload actually sent, so an edit that only changes name/occupation can't blank out address data it didn't include Deliberately NOT fixed here: economicStatus. The app sends it, but no persisted column for it exists on the beneficiary record in this schema -- it appears to belong on the household record (i_householddetails.type_bpl_apl) instead. Persisting it onto the beneficiary would be guessing at the wrong data model without confirmation. Verified live end-to-end against a real edit (benRegId 45387): occupation, community, pincode, block, village, and phone all confirmed landing correctly in the database. Household edit has a separate, more severe bug (full-object overwrite silently wipes street/colony/village on any edit) -- out of scope for this commit, tracked separately. Co-Authored-By: Claude Sonnet 5 * chore: bump version to 3.9.0 pom.xml still said 3.8.3 on this branch (cut from release-3.9.0) -- version string was never bumped when the branch was created. Co-Authored-By: Claude Sonnet 5 * fix: sync beneficiary economicStatus/residentialArea on edit (STOP-444) Same class of bug as the earlier occupation/community fix, found while investigating two more fields reported not updating: "type of residential area" and "socioeconomic status". Both economicStatus/economicStatusId and residentialArea/residentialAreaId are real columns on i_beneficiarydetails -- confirmed 100% NULL across every row in the table, including freshly created ones, meaning no code path (create or edit) had ever written to them. The app already sends both values every sync, nested inside i_bendemographics. residentialArea/residentialAreaId were initially (incorrectly) placed on RMNCHMBeneficiaryaddress in an earlier pass of this fix -- caught and corrected by cross-checking against FLW-API's own RMNCHMBeneficiarydetail, which already declares both fields on i_beneficiarydetails. That mistake never shipped; caught before this branch was ever restarted with it. Every field touched across both fix commits (community, occupation, religion, economicStatus, residentialArea, pinCode, block, district, village, phone) has now been cross-verified against FLW-API's independently-built entity model for the same tables, not just assumed. Co-Authored-By: Claude Sonnet 5 --------- Co-authored-by: Claude Sonnet 5 --- .../rmnch/RMNCHBeneficiaryDetailsRmnch.java | 12 +++++++ .../data/rmnch/RMNCHMBeneficiarydetail.java | 18 +++++++++++ .../rmnch/RmnchDataSyncServiceImpl.java | 32 +++++++++++++++++-- 3 files changed, 59 insertions(+), 3 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 60805dcf..b41c7ecc 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 @@ -422,6 +422,18 @@ public class RMNCHBeneficiaryDetailsRmnch { private String phoneNo; @Expose @Transient + private String economicStatus; + @Expose + @Transient + private Integer economicStatusId; + @Expose + @Transient + private String residentialArea; + @Expose + @Transient + private Integer residentialAreaId; + @Expose + @Transient private String contact_number; @Expose @Transient diff --git a/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHMBeneficiarydetail.java b/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHMBeneficiarydetail.java index 0e86127e..52ce4297 100644 --- a/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHMBeneficiarydetail.java +++ b/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHMBeneficiarydetail.java @@ -111,6 +111,24 @@ public class RMNCHMBeneficiarydetail { @Column(length = 45) private String occupation; + // STOP-444 followup fix: column exists (economicStatus/economicStatusId on + // i_beneficiarydetails) and is 100% NULL across every row, including freshly + // created ones — confirmed no code path, create or edit, has ever written to it. + // The app sends this value (inside i_bendemographics) every time. + private Integer economicStatusId; + + @Column(length = 45) + private String economicStatus; + + // STOP-444 followup fix: same situation as economicStatus above — real column, + // never written. Originally (wrongly) placed on the address entity in an earlier + // pass; corrected here to match where FLW-API's own RMNCHMBeneficiarydetail already + // declares it, confirming this is the real table. + private Integer residentialAreaId; + + @Column(name = "residentialArea") + private String residentialArea; + private Integer phcId; @Column(length = 30) 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 3a2674aa..6201d6a3 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 @@ -223,9 +223,7 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex if (demog.has("isGpsUnavailable") && !demog.get("isGpsUnavailable").isJsonNull()) obj.setIsGpsUnavailable(demog.get("isGpsUnavailable").getAsBoolean()); // STOP-444 followup fix: same source object, previously never read for - // anything but GPS. economicStatus deliberately NOT extracted here — its - // correct persisted home is unconfirmed (looks like it belongs on the - // household record, i_householddetails.type_bpl_apl, not the beneficiary). + // anything but GPS. if (demog.has("occupation") && !demog.get("occupation").isJsonNull()) obj.setOccupation(demog.get("occupation").getAsString()); if (demog.has("communityName") && !demog.get("communityName").isJsonNull()) @@ -236,6 +234,18 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex obj.setPinCode(demog.get("pinCode").getAsString()); if (demog.has("blockID") && !demog.get("blockID").isJsonNull()) obj.setBlockId(demog.get("blockID").getAsInt()); + // STOP-444 followup fix: economicStatus previously skipped here — turned + // out i_beneficiarydetails.economicStatus/economicStatusId genuinely exist + // (confirmed via schema, not just source code — 100% NULL across every row + // including fresh ones), so this is the correct persisted home after all. + if (demog.has("economicStatus") && !demog.get("economicStatus").isJsonNull()) + obj.setEconomicStatus(demog.get("economicStatus").getAsString()); + if (demog.has("economicStatusId") && !demog.get("economicStatusId").isJsonNull()) + obj.setEconomicStatusId(demog.get("economicStatusId").getAsInt()); + if (demog.has("residentialArea") && !demog.get("residentialArea").isJsonNull()) + obj.setResidentialArea(demog.get("residentialArea").getAsString()); + if (demog.has("residentialAreaId") && !demog.get("residentialAreaId").isJsonNull()) + obj.setResidentialAreaId(demog.get("residentialAreaId").getAsInt()); } // STOP-444 followup fix: phone number is sent as benPhoneMaps[0].phoneNo, not // a flat field — same lowercase-key mismatch problem as village/block below. @@ -342,6 +352,22 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex if (obj.getReligionID() != null) { rmnchmBeneficiarydetail.setReligionID(obj.getReligionID()); } + if (obj.getEconomicStatus() != null) { + rmnchmBeneficiarydetail.setEconomicStatus(obj.getEconomicStatus()); + } + if (obj.getEconomicStatusId() != null) { + rmnchmBeneficiarydetail.setEconomicStatusId(obj.getEconomicStatusId()); + } + // STOP-444 followup fix: residentialArea/residentialAreaId — corrected here + // from an earlier (wrong) placement on the address entity; confirmed against + // FLW-API's own RMNCHMBeneficiarydetail, which already declares these fields + // on this same table. + if (obj.getResidentialArea() != null) { + rmnchmBeneficiarydetail.setResidentialArea(obj.getResidentialArea()); + } + if (obj.getResidentialAreaId() != null) { + rmnchmBeneficiarydetail.setResidentialAreaId(obj.getResidentialAreaId()); + } if(obj.getFamilyId()!=null && !obj.getFamilyId().isEmpty()){ rmnchmBeneficiarydetail.setFamilyId(obj.getFamilyId()); From 1d24ba18a7454519273aaaa8ced037e5f8f07229 Mon Sep 17 00:00:00 2001 From: vanitha1822 Date: Tue, 22 Sep 2026 11:37:11 +0530 Subject: [PATCH 8/8] fix: resolve merge conflicts with main --- pom.xml | 2 +- .../rmnch/RmnchDataSyncServiceImpl.java | 257 ++++++++++++++++++ 2 files changed, 258 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 9eda8258..04fad9b0 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.iemr.common.identity identity-api - 3.9.0 + 3.8.3 war 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 6201d6a3..00162e33 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 @@ -25,6 +25,7 @@ import java.sql.Date; import java.sql.Timestamp; import java.text.SimpleDateFormat; +import java.text.SimpleDateFormat; import java.time.Period; import java.util.ArrayList; import java.util.Arrays; @@ -42,11 +43,13 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.http.*; +import org.springframework.http.*; import org.springframework.stereotype.Service; 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.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; @@ -76,6 +79,8 @@ 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.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; @@ -83,6 +88,8 @@ import com.iemr.common.identity.utils.mapper.InputMapper; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; @Service @Qualifier("rmnchServiceImpl") @@ -132,6 +139,27 @@ public class RmnchDataSyncServiceImpl implements RmnchDataSyncService { @Value("${stoptb.van.id}") private int configuredVanID; + // When true, sync fails loudly if camp is not configured instead of silently + // skipping vanID stamping. No inline default — every properties file must set this + // explicitly, so a forgotten config fails loudly at startup instead of running fail-open. + @Value("${stoptb.enforce.vanid}") + private boolean enforceVanID; + @Autowired + private BenDetailRepo benDetailRepo; + + @Value("${fhir-url}") + private String fhirUrl; + + // This deployment's van/camp ID. Previously looked up from Redis ("camp:vanID"), + // written at MMU login and deleted (globally, unscoped) on ANY user's logout — a Redis + // outage or an unrelated user's logout would silently break sync on this camp. Each + // camp/van already runs its own dedicated backend instance, so which van this is never + // actually changes at runtime; reading it from properties removes the Redis dependency + // entirely. No inline default — every properties file must set this explicitly. + // Scope: vanID only, parkingPlaceID is not part of this change. + @Value("${stoptb.van.id}") + private int configuredVanID; + // When true, sync fails loudly if camp is not configured instead of silently // skipping vanID stamping. No inline default — every properties file must set this // explicitly, so a forgotten config fails loudly at startup instead of running fail-open. @@ -139,6 +167,8 @@ public class RmnchDataSyncServiceImpl implements RmnchDataSyncService { private boolean enforceVanID; @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) @Override + public String syncDataToAmrit(String requestOBJ, String authorization) throws Exception { + public String syncDataToAmrit(String requestOBJ, String authorization) throws Exception { @@ -160,6 +190,17 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex final Integer vanID = campVanID; final Integer parkingPlaceID = null; + // Configured van ID for this deployment (see configuredVanID field javadoc above). + // parkingPlaceID is out of scope for this change — kept null, same as before whenever + // Redis had no value for it. + Integer campVanID = configuredVanID > 0 ? configuredVanID : null; + if (campVanID == null && enforceVanID) { + throw new Exception( + "Camp not configured: stoptb.van.id is 0. Set stoptb.van.id in this deployment's properties file."); + } + final Integer vanID = campVanID; + final Integer parkingPlaceID = null; + try { if (requestOBJ != null && !requestOBJ.isEmpty()) { JsonObject jsnOBJ = new JsonObject(); @@ -171,6 +212,8 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex // ben details RMNCH extra fields details logger.info("Request object of syncDataToAmrit: "+jsnOBJ); + logger.info("Request object of syncDataToAmrit: "+jsnOBJ); + BigInteger benRegID = null; @@ -294,6 +337,26 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex // else: trust the device-supplied CreatedDate as-is — offline captures // legitimately sync well after the actual event, so a later server time // would be less accurate than the device's own timestamp. + if (isPlausibleDeviceTimestamp(temp.getCreatedDate())) { + // Already has a good CreatedDate from the first sync — a later + // re-sync must never overwrite it. + obj.setCreatedDate(temp.getCreatedDate()); + } else if (isPlausibleDeviceTimestamp(obj.getCreatedDate())) { + // Stored value was garbage but this re-sync brought a plausible + // one from the device — self-heal using it (obj already has it). + } else { + obj.setCreatedDate(new Timestamp(System.currentTimeMillis())); + } + } + } else if (!isPlausibleDeviceTimestamp(obj.getCreatedDate())) { + // Device clock is broken (unset -> 1970 epoch, or set ahead -> future + // date). We can't recover the true capture time, so fall back to the + // sync time as the least-wrong value instead of storing garbage. + obj.setCreatedDate(new Timestamp(System.currentTimeMillis())); + } + // else: trust the device-supplied CreatedDate as-is — offline captures + // legitimately sync well after the actual event, so a later server time + // would be less accurate than the device's own timestamp. @@ -315,6 +378,12 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex obj.setVanID(vanID); obj.setParkingPlaceID(parkingPlaceID); } + // Mobile sends VanID=0 as a placeholder (not null) for a fresh record — + // `== null` alone never catches it, leaving the placeholder in place. + if ((obj.getVanID() == null || obj.getVanID() == 0) && vanID != null) { + obj.setVanID(vanID); + obj.setParkingPlaceID(parkingPlaceID); + } if(!rMNCHBenDetailsRepo.getByBenRegID(obj.getBenRegId()).isEmpty()){ RMNCHMBeneficiarydetail rmnchmBeneficiarydetail = rMNCHBenDetailsRepo.getByBenRegID(obj.getBenRegId()).get(0); @@ -402,6 +471,10 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex .saveAll(benDetailsExtraList); // The `id`/VanSerialNo field is Gson-collision-prone (see repo javadoc) — // force it back to each row's own PK after save. + benDetailsExtraList.forEach((n) -> rMNCHBeneficiaryDetailsRmnchRepo + .updateVanSerialNo(n.getBeneficiaryDetails_RmnchId())); + // The `id`/VanSerialNo field is Gson-collision-prone (see repo javadoc) — + // force it back to each row's own PK after save. benDetailsExtraList.forEach((n) -> rMNCHBeneficiaryDetailsRmnchRepo .updateVanSerialNo(n.getBeneficiaryDetails_RmnchId())); @@ -425,6 +498,22 @@ 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 : benDetailsOriginalList) { + 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() @@ -442,6 +531,15 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex obj.setVanID(vanID); obj.setParkingPlaceID(parkingPlaceID); } + if(!rMNCHBornBirthDetailsRepo.getByRegID(benRegID).isEmpty()){ + RMNCHBornBirthDetails temp = rMNCHBornBirthDetailsRepo.getByRegID(benRegID).get(0); + if (temp != null) + obj.setBornBirthDeatilsId(temp.getBornBirthDeatilsId()); + } + if (obj.getVanID() == null && vanID != null) { + obj.setVanID(vanID); + obj.setParkingPlaceID(parkingPlaceID); + } } bornBirthList = (ArrayList) rMNCHBornBirthDetailsRepo .saveAll(bornBirthList); @@ -471,6 +569,15 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex obj.setVanID(vanID); obj.setParkingPlaceID(parkingPlaceID); } + if(!rMNCHCBACDetailsRepo.getByRegID(benRegID).isEmpty()){ + RMNCHCBACdetails temp = rMNCHCBACDetailsRepo.getByRegID(benRegID).get(0); + if (temp != null) + obj.setCBACDetailsid(temp.getCBACDetailsid()); + } + if (obj.getVanID() == null && vanID != null) { + obj.setVanID(vanID); + obj.setParkingPlaceID(parkingPlaceID); + } } cbacList = (ArrayList) rMNCHCBACDetailsRepo.saveAll(cbacList); @@ -499,6 +606,22 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex } catch (NumberFormatException ignored) {} } + // 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()){ @@ -513,6 +636,17 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex // stay OUTSIDE the "household already exists" block above (it's regressed // back inside there twice already via merges), otherwise a brand-new // household never gets VanID stamped, breaking van-scoped sync. + if (obj.getVanID() == null && vanID != null) { + obj.setVanID(vanID); + obj.setParkingPlaceID(parkingPlaceID); + } + if (hhTimestampMap.containsKey(obj.getHouseoldId())) + obj.setGpsTimestamp(new Timestamp(hhTimestampMap.get(obj.getHouseoldId()))); + } + // Set VanID/ParkingPlaceID for both NEW and existing households — this must + // stay OUTSIDE the "household already exists" block above (it's regressed + // back inside there twice already via merges), otherwise a brand-new + // household never gets VanID stamped, breaking van-scoped sync. if (obj.getVanID() == null && vanID != null) { obj.setVanID(vanID); obj.setParkingPlaceID(parkingPlaceID); @@ -522,6 +656,10 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex .saveAll(houseHoldList); // The `id`/VanSerialNo field is Gson-collision-prone (see repo javadoc) — // force it back to each row's own PK after save. + houseHoldList.forEach((n) -> rMNCHHouseHoldDetailsRepo + .updateVanSerialNo(n.getHouseHoldDetailsId())); + // The `id`/VanSerialNo field is Gson-collision-prone (see repo javadoc) — + // force it back to each row's own PK after save. houseHoldList.forEach((n) -> rMNCHHouseHoldDetailsRepo .updateVanSerialNo(n.getHouseHoldDetailsId())); // success response @@ -655,6 +793,110 @@ public String mapHealthIDToBeneficiary(String authorization, } + /** + * Splits a list into sub-lists (batches) of the given size. + * Last batch may contain fewer elements. + */ + private List> partitionList(List list, int batchSize) { + List> batches = new ArrayList<>(); + if (list == null || list.isEmpty()) { + return batches; + } + for (int i = 0; i < list.size(); i += batchSize) { + batches.add(new ArrayList<>(list.subList(i, Math.min(i + batchSize, list.size())))); + } + return batches; + } + + public String mapHealthIDToBeneficiary(String authorization, + Long benRegID, + Long beneficiaryID, + String abhaId, + String createdBy,String firstName,String lastName,String dob,Integer providerServiceMapId) { + try { + RestTemplate restTemplate = new RestTemplate(); + String formattedDob = dob; + + try { + if (dob != null && dob.contains(" ")) { + Timestamp timestamp = Timestamp.valueOf(dob); + formattedDob = new SimpleDateFormat("dd-MM-yyyy") + .format(timestamp); + } + } catch (Exception ex) { + logger.warn("DOB format conversion failed, sending original DOB : {}", dob); + } + logger.info("Authorization Token : {}", authorization); + + Map requestMap = new HashMap<>(); + + requestMap.put("beneficiaryRegID", benRegID); + requestMap.put("beneficiaryID", beneficiaryID); + requestMap.put("healthIdNumber", abhaId); + + requestMap.put("createdBy", createdBy); + requestMap.put("providerServiceMapId", providerServiceMapId); + requestMap.put("isNew", false); + + // ABHA Profile + Map abhaProfile = new HashMap<>(); + abhaProfile.put("ABHANumber", abhaId); + + List phrAddress = new ArrayList<>(); + phrAddress.add(abhaId + "@abdm"); + + abhaProfile.put("phrAddress", phrAddress); + abhaProfile.put("firstName", firstName); + abhaProfile.put("middleName", ""); + abhaProfile.put("lastName", lastName); + abhaProfile.put("dob", formattedDob); + + + requestMap.put("ABHAProfile", abhaProfile); + + String requestBody = new Gson().toJson(requestMap); + + String url = fhirUrl + + ConfigProperties.getPropertyByName("mapHealthIDToBeneficiary"); + + logger.info("Calling URL : {}", url); + logger.info("Request Body : {}", requestBody); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + headers.set("Jwttoken", authorization); + + HttpEntity entity = + new HttpEntity<>(requestBody, headers); + + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.POST, + entity, + String.class + ); + + logger.info("ABHA Mapping Response : {}", response.getBody()); + + return response.getBody(); + + } catch (HttpClientErrorException e) { + + logger.error("HTTP Error Status : {}", e.getStatusCode()); + logger.error("HTTP Error Response : {}", e.getResponseBodyAsString(), e); + + return "HTTP Error : " + e.getStatusCode(); + + } catch (Exception e) { + + logger.error("Error while saving Health ID Mapping", e); + + return "Error Save Health Id : " + e.getMessage(); + } + + } + @Override @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) @@ -1041,12 +1283,19 @@ private String getMappingsForAddressIDs(List addressLi benDetailsRMNCHOBJ = rMNCHBeneficiaryDetailsRmnchRepo .getByRegID(m.getBenRegId()).get(0); } + if(!rMNCHBornBirthDetailsRepo.getByRegID(m.getBenRegId()).isEmpty()){ + benBotnBirthRMNCHROBJ = rMNCHBornBirthDetailsRepo.getByRegID(m.getBenRegId()).get(0); if(!rMNCHBornBirthDetailsRepo.getByRegID(m.getBenRegId()).isEmpty()){ benBotnBirthRMNCHROBJ = rMNCHBornBirthDetailsRepo.getByRegID(m.getBenRegId()).get(0); } if(! rMNCHCBACDetailsRepo.getByRegID(m.getBenRegId()).isEmpty()){ benCABCRMNCHROBJ = rMNCHCBACDetailsRepo.getByRegID(m.getBenRegId()).get(0); + } + if(! rMNCHCBACDetailsRepo.getByRegID(m.getBenRegId()).isEmpty()){ + benCABCRMNCHROBJ = rMNCHCBACDetailsRepo.getByRegID(m.getBenRegId()).get(0); + + } } @@ -1076,6 +1325,12 @@ private String getMappingsForAddressIDs(List addressLi .getByHouseHoldID(benDetailsRMNCHOBJ.getHouseoldId()).get(0); } + if(!rMNCHHouseHoldDetailsRepo + .getByHouseHoldID(benDetailsRMNCHOBJ.getHouseoldId()).isEmpty()){ + benHouseHoldRMNCHROBJ = rMNCHHouseHoldDetailsRepo + .getByHouseHoldID(benDetailsRMNCHOBJ.getHouseoldId()).get(0); + } + } if (benDetailsRMNCHOBJ == null) @@ -1145,6 +1400,8 @@ private String getMappingsForAddressIDs(List addressLi benDetailsRMNCHOBJ.setAddressLine3(benAddressOBJ.getPermAddrLine3()); if (benAddressOBJ.getPermPinCode() != null) benDetailsRMNCHOBJ.setPinCode(benAddressOBJ.getPermPinCode()); + if (benAddressOBJ.getPermPinCode() != null) + benDetailsRMNCHOBJ.setPinCode(benAddressOBJ.getPermPinCode()); // related benids if (benDetailsRMNCHOBJ.getRelatedBeneficiaryIdsDB() != null) {