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 01/13] 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 02/13] 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 03/13] 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 04/13] 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 05/13] 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 06/13] 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 07/13] 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 367d924fb2fb2436a6d16bf4c633a930558329c2 Mon Sep 17 00:00:00 2001 From: Vanitha S <116701245+vanitha1822@users.noreply.github.com> Date: Thu, 24 Sep 2026 12:54:59 +0530 Subject: [PATCH 08/13] fix: benregid null issue in i_beneficiarydetails table (#194) From f17e9528e0cc95018bfda6d982f23c47e72856c9 Mon Sep 17 00:00:00 2001 From: chetansaroya Date: Fri, 9 Oct 2026 08:22:42 +0530 Subject: [PATCH 09/13] feat: stop-542 write data anthropometry and vitals table (#201) --- .../data/rmnch/BenAnthropometryDetail.java | 101 ++++++++++++++ .../data/rmnch/BenPhysicalVitalDetail.java | 107 +++++++++++++++ .../repo/rmnch/BenAnthropometryRepo.java | 41 ++++++ .../repo/rmnch/BenPhysicalVitalRepo.java | 41 ++++++ .../rmnch/RmnchDataSyncServiceImpl.java | 125 ++++++++++++++---- 5 files changed, 391 insertions(+), 24 deletions(-) create mode 100644 src/main/java/com/iemr/common/identity/data/rmnch/BenAnthropometryDetail.java create mode 100644 src/main/java/com/iemr/common/identity/data/rmnch/BenPhysicalVitalDetail.java create mode 100644 src/main/java/com/iemr/common/identity/repo/rmnch/BenAnthropometryRepo.java create mode 100644 src/main/java/com/iemr/common/identity/repo/rmnch/BenPhysicalVitalRepo.java diff --git a/src/main/java/com/iemr/common/identity/data/rmnch/BenAnthropometryDetail.java b/src/main/java/com/iemr/common/identity/data/rmnch/BenAnthropometryDetail.java new file mode 100644 index 00000000..27a6df6e --- /dev/null +++ b/src/main/java/com/iemr/common/identity/data/rmnch/BenAnthropometryDetail.java @@ -0,0 +1,101 @@ +/* +* 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; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import lombok.Data; + +/** + * Mirrors FLW-API's BenAnthropometryDetail (same db_iemr.t_phy_anthropometry table), so syncDataToAmrit can write + * registration-time values that FLW-API getBeneficiaryData reads back. + */ +@Entity +@Table(name = "t_phy_anthropometry", catalog = "db_iemr") +@Data +public class BenAnthropometryDetail { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "ID") + private Long id; + + @Column(name = "BeneficiaryRegID") + private Long beneficiaryRegID; + + @Column(name = "BenVisitID") + private Long benVisitID; + + @Column(name = "ProviderServiceMapID") + private Integer providerServiceMapID; + + @Column(name = "VisitCode") + private Long visitCode; + + @Column(name = "Weight_Kg") + private Double weightKg; + + @Column(name = "Height_cm") + private Double heightCm; + + @Column(name = "BMI") + private Double bmi; + + @Column(name = "Deleted", insertable = false, updatable = true) + private Boolean deleted; + + @Column(name = "Processed", insertable = false, updatable = true) + private String processed; + + @Column(name = "CreatedBy") + private String createdBy; + + @Column(name = "CreatedDate", insertable = false, updatable = false) + private Timestamp createdDate; + + @Column(name = "ModifiedBy") + private String modifiedBy; + + @Column(name = "LastModDate", insertable = false, updatable = false) + private Timestamp lastModDate; + + @Column(name = "VanSerialNo") + private Long vanSerialNo; + + @Column(name = "VanID") + private Integer vanID; + + @Column(name = "ParkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "SyncedBy") + private String syncedBy; + + @Column(name = "SyncedDate") + private Timestamp syncedDate; +} diff --git a/src/main/java/com/iemr/common/identity/data/rmnch/BenPhysicalVitalDetail.java b/src/main/java/com/iemr/common/identity/data/rmnch/BenPhysicalVitalDetail.java new file mode 100644 index 00000000..9e98f289 --- /dev/null +++ b/src/main/java/com/iemr/common/identity/data/rmnch/BenPhysicalVitalDetail.java @@ -0,0 +1,107 @@ +/* +* 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; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import lombok.Data; + +/** + * Mirrors FLW-API's BenPhysicalVitalDetail (same db_iemr.t_phy_vitals table), so syncDataToAmrit can write + * registration-time values that FLW-API getBeneficiaryData reads back. + */ +@Entity +@Table(name = "t_phy_vitals", catalog = "db_iemr") +@Data +public class BenPhysicalVitalDetail { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "ID") + private Long id; + + @Column(name = "BeneficiaryRegID") + private Long beneficiaryRegID; + + @Column(name = "BenVisitID") + private Long benVisitID; + + @Column(name = "ProviderServiceMapID") + private Integer providerServiceMapID; + + @Column(name = "VisitCode") + private Long visitCode; + + @Column(name = "Temperature") + private Double temperature; + + @Column(name = "PulseRate") + private Short pulseRate; + + @Column(name = "SystolicBP_1stReading") + private Short systolicBP; + + @Column(name = "DiastolicBP_1stReading") + private Short diastolicBP; + + @Column(name = "BloodGlucose_Random") + private Short bloodGlucoseRandom; + + @Column(name = "Deleted", insertable = false, updatable = true) + private Boolean deleted; + + @Column(name = "Processed", insertable = false, updatable = true) + private String processed; + + @Column(name = "CreatedBy") + private String createdBy; + + @Column(name = "CreatedDate", insertable = false, updatable = false) + private Timestamp createdDate; + + @Column(name = "ModifiedBy") + private String modifiedBy; + + @Column(name = "LastModDate", insertable = false, updatable = false) + private Timestamp lastModDate; + + @Column(name = "VanSerialNo") + private Long vanSerialNo; + + @Column(name = "VanID") + private Integer vanID; + + @Column(name = "ParkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "SyncedBy") + private String syncedBy; + + @Column(name = "SyncedDate") + private Timestamp syncedDate; +} diff --git a/src/main/java/com/iemr/common/identity/repo/rmnch/BenAnthropometryRepo.java b/src/main/java/com/iemr/common/identity/repo/rmnch/BenAnthropometryRepo.java new file mode 100644 index 00000000..95476f67 --- /dev/null +++ b/src/main/java/com/iemr/common/identity/repo/rmnch/BenAnthropometryRepo.java @@ -0,0 +1,41 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.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.BenAnthropometryDetail; + +@Repository +public interface BenAnthropometryRepo extends CrudRepository { + public BenAnthropometryDetail findFirstByBeneficiaryRegIDOrderByCreatedDateDescIdDesc(Long beneficiaryRegID); + + @Transactional + @Modifying + @Query("UPDATE BenAnthropometryDetail t SET t.vanSerialNo = t.id WHERE t.id = :id") + void updateVanSerialNo(@Param("id") Long id); +} diff --git a/src/main/java/com/iemr/common/identity/repo/rmnch/BenPhysicalVitalRepo.java b/src/main/java/com/iemr/common/identity/repo/rmnch/BenPhysicalVitalRepo.java new file mode 100644 index 00000000..61ddc045 --- /dev/null +++ b/src/main/java/com/iemr/common/identity/repo/rmnch/BenPhysicalVitalRepo.java @@ -0,0 +1,41 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.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.BenPhysicalVitalDetail; + +@Repository +public interface BenPhysicalVitalRepo extends CrudRepository { + public BenPhysicalVitalDetail findFirstByBeneficiaryRegIDAndTemperatureIsNotNullOrderByCreatedDateDescIdDesc(Long beneficiaryRegID); + + @Transactional + @Modifying + @Query("UPDATE BenPhysicalVitalDetail t SET t.vanSerialNo = t.id WHERE t.id = :id") + void updateVanSerialNo(@Param("id") Long id); +} 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..c8f3b265 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 @@ -51,7 +51,9 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.iemr.common.identity.controller.rmnch.RMNCHMobileAppController; +import com.iemr.common.identity.data.rmnch.BenAnthropometryDetail; import com.iemr.common.identity.data.rmnch.BenHealthIDDetails; +import com.iemr.common.identity.data.rmnch.BenPhysicalVitalDetail; import com.iemr.common.identity.data.rmnch.GetBenRequestHandler; import com.iemr.common.identity.data.rmnch.NcdTbHrpData; import com.iemr.common.identity.data.rmnch.RMNCHBeneficiaryDetailsRmnch; @@ -64,6 +66,8 @@ import com.iemr.common.identity.data.rmnch.RMNCHMBeneficiarycontact; import com.iemr.common.identity.data.rmnch.RMNCHMBeneficiarydetail; import com.iemr.common.identity.data.rmnch.RMNCHMBeneficiarymapping; +import com.iemr.common.identity.repo.rmnch.BenAnthropometryRepo; +import com.iemr.common.identity.repo.rmnch.BenPhysicalVitalRepo; import com.iemr.common.identity.repo.rmnch.RMNCHBenAccountRepo; import com.iemr.common.identity.repo.rmnch.RMNCHBenAddressRepo; import com.iemr.common.identity.repo.rmnch.RMNCHBenContactRepo; @@ -118,6 +122,10 @@ public class RmnchDataSyncServiceImpl implements RmnchDataSyncService { private RMNCHMBenRegIdMapRepo rMNCHMBenRegIdMapRepo; @Autowired private BenDetailRepo benDetailRepo; + @Autowired + private BenAnthropometryRepo benAnthropometryRepo; + @Autowired + private BenPhysicalVitalRepo benPhysicalVitalRepo; @Value("${fhir-url}") private String fhirUrl; @@ -409,19 +417,13 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex // update beneficiary data in i_beneficiarydetails table rMNCHBenDetailsRepo.saveAll(benDetailsList); - // Write anthropometry (height/weight/bmi/temperature) to i_beneficiarydetails.otherFields. - // i_beneficiarydetails_rmnch has no these columns; FLW-API getBeneficiaryData reads from otherFields. + // Write anthropometry (height/weight/bmi/temperature) to db_iemr.t_phy_anthropometry / + // t_phy_vitals — i_beneficiarydetails_rmnch has no these columns. FLW-API getBeneficiaryData + // and the nurse exam save read them from there. 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()); - } + writeAnthropometry(obj); + writeVitals(obj); } } @@ -881,20 +883,95 @@ private boolean hasAnthropometryData(RMNCHBeneficiaryDetailsRmnch obj) { || 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) { - } + // Inserts a new t_phy_anthropometry row only when height/weight/bmi changed vs the latest row, + // carrying forward any value the app didn't send — from the latest row, or from otherFields for + // beneficiaries whose values were stored there before this table was written. Not tied to a visit — + // BenVisitID/VisitCode are nullable, and FLW-API reads latest by BeneficiaryRegID. + // Not wrapped in try/catch: these tables are now the only store for these values, and a failed save + // marks the sync transaction rollback-only anyway, so let the sync fail and the app retry. + private void writeAnthropometry(RMNCHBeneficiaryDetailsRmnch obj) { + if (obj.getHeight() == null && obj.getWeight() == null && obj.getBmi() == null) + return; + Long benRegID = obj.getBenRegId().longValue(); + if (obj.getCreatedBy() == null) { + logger.warn("Skipping t_phy_anthropometry write, createdBy missing for benRegId: " + benRegID); + return; + } + BenAnthropometryDetail latest = benAnthropometryRepo + .findFirstByBeneficiaryRegIDOrderByCreatedDateDescIdDesc(benRegID); + JsonObject legacy = latest == null ? getLegacyAnthropometry(obj.getBenRegId()) : new JsonObject(); + Double height = obj.getHeight() != null ? obj.getHeight() + : (latest != null ? latest.getHeightCm() : getDouble(legacy, "height")); + Double weight = obj.getWeight() != null ? obj.getWeight() + : (latest != null ? latest.getWeightKg() : getDouble(legacy, "weight")); + Double bmi = obj.getBmi() != null ? obj.getBmi() : (latest != null ? latest.getBmi() : getDouble(legacy, "bmi")); + if (latest != null && sameValue(latest.getHeightCm(), height) && sameValue(latest.getWeightKg(), weight) + && sameValue(latest.getBmi(), bmi)) + return; + + BenAnthropometryDetail a = new BenAnthropometryDetail(); + a.setBeneficiaryRegID(benRegID); + a.setProviderServiceMapID(obj.getProviderServiceMapID()); + a.setCreatedBy(obj.getCreatedBy()); + a.setVanID(obj.getVanID()); + a.setParkingPlaceID(obj.getParkingPlaceID()); + a.setHeightCm(height); + a.setWeightKg(weight); + a.setBmi(bmi); + a = benAnthropometryRepo.save(a); + benAnthropometryRepo.updateVanSerialNo(a.getId()); + } + + // Inserts a new t_phy_vitals row only when temperature changed vs the latest recorded temperature. + // Pulse/BP/glucose aren't part of registration, so they stay null here. + private void writeVitals(RMNCHBeneficiaryDetailsRmnch obj) { + if (obj.getTemperature() == null) + return; + Long benRegID = obj.getBenRegId().longValue(); + if (obj.getCreatedBy() == null) { + logger.warn("Skipping t_phy_vitals write, createdBy missing for benRegId: " + benRegID); + return; + } + BenPhysicalVitalDetail latest = benPhysicalVitalRepo + .findFirstByBeneficiaryRegIDAndTemperatureIsNotNullOrderByCreatedDateDescIdDesc(benRegID); + if (latest != null && sameValue(latest.getTemperature(), obj.getTemperature())) + return; + + BenPhysicalVitalDetail v = new BenPhysicalVitalDetail(); + v.setBeneficiaryRegID(benRegID); + v.setProviderServiceMapID(obj.getProviderServiceMapID()); + v.setCreatedBy(obj.getCreatedBy()); + v.setVanID(obj.getVanID()); + v.setParkingPlaceID(obj.getParkingPlaceID()); + v.setTemperature(obj.getTemperature()); + v = benPhysicalVitalRepo.save(v); + benPhysicalVitalRepo.updateVanSerialNo(v.getId()); + } + + // Columns are DECIMAL(5,2), so compare at that precision. + private boolean sameValue(Double stored, Double incoming) { + if (stored == null || incoming == null) + return stored == incoming; + return Math.round(stored * 100) == Math.round(incoming * 100); + } + + private JsonObject getLegacyAnthropometry(BigInteger benRegID) { + try { + MBeneficiarydetail benDetail = benDetailRepo.findByBenRegId(benRegID); + if (benDetail != null && benDetail.getOtherFields() != null && !benDetail.getOtherFields().isBlank()) + return new JsonParser().parse(benDetail.getOtherFields()).getAsJsonObject(); + } catch (Exception ex) { + logger.warn("Cannot read otherFields for benRegId: " + benRegID + " - " + ex.getMessage()); + } + return new JsonObject(); + } + + private Double getDouble(JsonObject json, String key) { + try { + return json.has(key) && !json.get(key).isJsonNull() ? json.get(key).getAsDouble() : null; + } catch (Exception ex) { + return null; } - 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 From 5634e6d570515ecaa4b931e959ba5e16e0d50d61 Mon Sep 17 00:00:00 2001 From: chetansaroya Date: Fri, 9 Oct 2026 09:59:24 +0530 Subject: [PATCH 10/13] Bugfix/stop 542 (#202) * feat: stop-542 write data anthropometry and vitals table * feat(STOP-542): update version to 3.12.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 9eda8258..667d7cc6 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.iemr.common.identity identity-api - 3.9.0 + 3.12.0 war From 3f91c090051e26bc79109d09c1dc626cb55165ac Mon Sep 17 00:00:00 2001 From: Sehjot Singh Pannu Date: Fri, 9 Oct 2026 15:48:11 +0530 Subject: [PATCH 11/13] Removing gpsLatitude and gpsLongitude, and setting values into latitude and longitude --- .../identity/data/rmnch/RMNCHBeneficiaryDetailsRmnch.java | 8 -------- .../identity/service/rmnch/RmnchDataSyncServiceImpl.java | 4 ++-- 2 files changed, 2 insertions(+), 10 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 b41c7ecc..2d80fc8f 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 @@ -611,14 +611,6 @@ public class RMNCHBeneficiaryDetailsRmnch { @Transient private Double temperature; // stored as "temperatureValue" in otherFields to match getBeneficiaryData key - @Expose - @Column(name = "gpsLatitude") - private Double gpsLatitude; - - @Expose - @Column(name = "gpsLongitude") - private Double gpsLongitude; - @Expose @Column(name = "digipin") private String digipin; 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 c8f3b265..e4062bb3 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 @@ -221,9 +221,9 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex JsonObject demog = benGpsMap.get(obj.getBenficieryid()); if (demog != null) { if (demog.has("latitude") && !demog.get("latitude").isJsonNull()) - obj.setGpsLatitude(demog.get("latitude").getAsDouble()); + obj.setLatitude(demog.get("latitude").getAsBigDecimal()); if (demog.has("longitude") && !demog.get("longitude").isJsonNull()) - obj.setGpsLongitude(demog.get("longitude").getAsDouble()); + obj.setLongitude(demog.get("longitude").getAsBigDecimal()); if (demog.has("digipin") && !demog.get("digipin").isJsonNull()) obj.setDigipin(demog.get("digipin").getAsString()); if (demog.has("gpsTimestamp") && !demog.get("gpsTimestamp").isJsonNull()) From ad8e38d5f6cfba13b6c4ddeff1b214a77e96c800 Mon Sep 17 00:00:00 2001 From: Vishwanath Balkur <118195001+vishwab1@users.noreply.github.com> Date: Wed, 14 Oct 2026 15:48:25 +0530 Subject: [PATCH 12/13] fix(STOP-457): safe repeat import and correct free BenID count (#206) - saveGeneratedBenIDToLocalServer uses INSERT IGNORE, so importing the same IDs again (retry after a failed or partial import) skips existing rows instead of failing; returns the number really inserted. - checkAvailablBenIDLocalServer counts only IDs that are neither provisioned nor reserved by a registration in progress. Co-authored-by: Claude Opus 5.5 --- .../controller/IdentityController.java | 2 ++ .../identity/repo/BenRegIdMappingRepo.java | 6 ++++ .../identity/service/IdentityService.java | 28 +++++++++++++++++-- 3 files changed, 33 insertions(+), 3 deletions(-) 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 2482ac47..6468ed3e 100644 --- a/src/main/java/com/iemr/common/identity/controller/IdentityController.java +++ b/src/main/java/com/iemr/common/identity/controller/IdentityController.java @@ -990,6 +990,8 @@ public String saveGeneratedBenIDToLocalServer( logger.info("Number of unique benid imported to local server: " + i); if (i > 0) response.setResponse(i + " Unique benid imported to local server"); + else if (!benIdImportDTOList.isEmpty()) + response.setResponse("All benid already present in local server"); else { response.setResponse("Empty or invalid data"); logger.error("Empty or invalid data. Data Size is : " + benIdImportDTOList.size()); diff --git a/src/main/java/com/iemr/common/identity/repo/BenRegIdMappingRepo.java b/src/main/java/com/iemr/common/identity/repo/BenRegIdMappingRepo.java index 866b2137..ea629982 100644 --- a/src/main/java/com/iemr/common/identity/repo/BenRegIdMappingRepo.java +++ b/src/main/java/com/iemr/common/identity/repo/BenRegIdMappingRepo.java @@ -60,6 +60,12 @@ MBeneficiaryregidmapping findFirstByProviderServiceMapIDAndVehicalNoOrderByBenRe Long countByProvisioned(Boolean isProvisioned); + /** + * Count of BenRegIds that can be given to a new registration: not provisioned + * and not claimed by a registration in progress. + */ + Long countByProvisionedAndReserved(Boolean isProvisioned, Boolean isReserved); + @Transactional @Modifying @Query("update MBeneficiaryregidmapping set providerServiceMapID = null , vehicalNo = null " 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 0d965dd5..f1872cb1 100644 --- a/src/main/java/com/iemr/common/identity/service/IdentityService.java +++ b/src/main/java/com/iemr/common/identity/service/IdentityService.java @@ -2170,7 +2170,9 @@ public int importBenIdToLocalServer(List benIdImportDTOList) { jdbcTemplate = getJdbcTemplate(); List dataList = new ArrayList<>(); Object[] objArr; - String query = " INSERT INTO m_beneficiaryregidmapping(BenRegId, BeneficiaryID, " + // INSERT IGNORE: importing the same IDs again (retry after a failed or partial + // import) skips rows already present instead of failing the whole batch + String query = " INSERT IGNORE INTO m_beneficiaryregidmapping(BenRegId, BeneficiaryID, " + " Provisioned, CreatedDate, CreatedBy, Reserved, vanID) VALUES (?,?,?,?,?,?,?) "; logger.info("query : " + query); for (MBeneficiaryregidmapping obj : mBeneficiaryregidmappingList) { @@ -2191,9 +2193,27 @@ public int importBenIdToLocalServer(List benIdImportDTOList) { logger.info("regid :" + obj.getBenRegId() + " - benid :" + obj.getBeneficiaryID()); } + // batched inserts do not report per-row results, so count the rows in this + // BenRegId range before and after to know how many were really added + java.math.BigInteger minId = null; + java.math.BigInteger maxId = null; + for (MBeneficiaryregidmapping obj : mBeneficiaryregidmappingList) { + java.math.BigInteger id = obj.getBenRegId(); + if (id != null) { + minId = (minId == null || id.compareTo(minId) < 0) ? id : minId; + maxId = (maxId == null || id.compareTo(maxId) > 0) ? id : maxId; + } + } + String rangeCount = "SELECT COUNT(*) FROM m_beneficiaryregidmapping WHERE BenRegId BETWEEN ? AND ?"; + Long before = minId == null ? 0L : jdbcTemplate.queryForObject(rangeCount, Long.class, minId, maxId); + int[] i = jdbcTemplate.batchUpdate(query, dataList); - return i.length; + Long after = minId == null ? 0L : jdbcTemplate.queryForObject(rangeCount, Long.class, minId, maxId); + int inserted = (int) (after - before); + logger.info("IdentityService.importBenIdToLocalServer - received " + i.length + ", newly inserted " + + inserted + ", already present or skipped " + (i.length - inserted)); + return inserted; } else { return 0; } @@ -2201,7 +2221,9 @@ public int importBenIdToLocalServer(List benIdImportDTOList) { } public Long checkBenIDAvailabilityLocal() { - return regIdRepo.countByProvisioned(false); + // Provisioned=false alone also counts rows claimed (Reserved=true) by a registration + // that never completed; those cannot be given to a new registration + return regIdRepo.countByProvisionedAndReserved(false, false); } From 4f2afa09b9869374c249278caddad8d7c0e7318c Mon Sep 17 00:00:00 2001 From: Sehjot Singh Pannu Date: Mon, 2 Nov 2026 16:00:48 +0530 Subject: [PATCH 13/13] removed claude files --- .claude/commands/new-bugfix.md | 46 -- .claude/commands/new-feature.md | 45 -- .claude/commands/new-hotfix.md | 52 -- .claude/commands/pr-checklist.md | 167 ----- .claude/skills/java-code-reviewer/SKILL.md | 247 -------- .../agents/architecture-reviewer.md | 181 ------ .../agents/security-auditor.md | 145 ----- .../agents/test-reviewer.md | 226 ------- .../references/common-issues.md | 397 ------------ .../references/review-checklist.md | 157 ----- .claude/skills/java-code-writer/SKILL.md | 156 ----- .../java-code-writer/references/exceptions.md | 158 ----- .../references/factory-patterns.md | 364 ----------- .../references/security-patterns.md | 197 ------ .../references/spring-patterns.md | 584 ------------------ 15 files changed, 3122 deletions(-) delete mode 100644 .claude/commands/new-bugfix.md delete mode 100644 .claude/commands/new-feature.md delete mode 100644 .claude/commands/new-hotfix.md delete mode 100644 .claude/commands/pr-checklist.md delete mode 100644 .claude/skills/java-code-reviewer/SKILL.md delete mode 100644 .claude/skills/java-code-reviewer/agents/architecture-reviewer.md delete mode 100644 .claude/skills/java-code-reviewer/agents/security-auditor.md delete mode 100644 .claude/skills/java-code-reviewer/agents/test-reviewer.md delete mode 100644 .claude/skills/java-code-reviewer/references/common-issues.md delete mode 100644 .claude/skills/java-code-reviewer/references/review-checklist.md delete mode 100644 .claude/skills/java-code-writer/SKILL.md delete mode 100644 .claude/skills/java-code-writer/references/exceptions.md delete mode 100644 .claude/skills/java-code-writer/references/factory-patterns.md delete mode 100644 .claude/skills/java-code-writer/references/security-patterns.md delete mode 100644 .claude/skills/java-code-writer/references/spring-patterns.md diff --git a/.claude/commands/new-bugfix.md b/.claude/commands/new-bugfix.md deleted file mode 100644 index a3ab0ad1..00000000 --- a/.claude/commands/new-bugfix.md +++ /dev/null @@ -1,46 +0,0 @@ -# /new-bugfix - -Create a new bugfix branch targeting the current release branch. - -## Steps - -1. Ask the user for: - - The Jira ticket ID (e.g., `AMRIT-456`) - - A short description of the bug being fixed (3–5 words, kebab-case) - - Which release branch to target (default: latest `release-X.Y.Z`) - -2. Determine available release branches: - ```bash - git branch -r | grep 'release-' | sort -V - ``` - -3. Ensure the target release branch is up to date: - ```bash - git fetch origin - git checkout - git pull origin - ``` - -4. Create and push the bugfix branch: - ```bash - git checkout -b bugfix/- - git push -u origin bugfix/- - ``` - -5. Confirm the branch was created: - ```bash - git branch --show-current - ``` - -## Branch Naming Rules - -- Pattern: `bugfix/AMRIT--` -- Slug: lowercase kebab-case, max 5 words, describes the bug (not the fix) -- Example: `bugfix/AMRIT-456-null-pointer-in-anc-save` - -## Notes - -- Bugfix branches target `release-X.Y.Z`, NOT `main`. -- If the fix is urgent and needs to go to `main` immediately, use `/new-hotfix` instead. -- If the user has uncommitted changes, warn them and ask whether to stash before switching. -- After branching, remind the user to write a failing test that reproduces the bug before implementing the fix. diff --git a/.claude/commands/new-feature.md b/.claude/commands/new-feature.md deleted file mode 100644 index 1fbab454..00000000 --- a/.claude/commands/new-feature.md +++ /dev/null @@ -1,45 +0,0 @@ -# /new-feature - -Create a new feature branch following the AMRIT branching strategy. - -## Steps - -1. Ask the user for: - - The Jira ticket ID (e.g., `AMRIT-123`) - - A short description (3–5 words, kebab-case) - -2. Determine the current release branch: - ```bash - git branch -r | grep 'release-' | sort -V | tail -1 - ``` - If no release branch exists, use `main` and warn the user. - -3. Ensure the local release branch is up to date: - ```bash - git fetch origin - git checkout - git pull origin - ``` - -4. Create and push the feature branch: - ```bash - git checkout -b feature/- - git push -u origin feature/- - ``` - -5. Confirm the branch was created: - ```bash - git branch --show-current - ``` - -## Branch Naming Rules - -- Pattern: `feature/AMRIT--` -- Slug: lowercase kebab-case, max 5 words -- Example: `feature/AMRIT-123-add-cbac-screening` - -## Notes - -- Feature branches MUST be cut from the current `release-X.Y.Z` branch, not from `main`. -- If the user has uncommitted changes, warn them and ask whether to stash before switching. -- Never push directly to `main` or `release-X.Y.Z`. \ No newline at end of file diff --git a/.claude/commands/new-hotfix.md b/.claude/commands/new-hotfix.md deleted file mode 100644 index 46234c7b..00000000 --- a/.claude/commands/new-hotfix.md +++ /dev/null @@ -1,52 +0,0 @@ -# /new-hotfix - -Create a hotfix branch from `main` for urgent production fixes. - -## Steps - -1. Ask the user for: - - The Jira ticket ID (e.g., `AMRIT-789`) - - A short description of the production issue (3–5 words, kebab-case) - - Confirmation that this is a production incident (not a regular bugfix) - -2. Ensure `main` is up to date: - ```bash - git fetch origin - git checkout main - git pull origin main - ``` - -3. Create and push the hotfix branch: - ```bash - git checkout -b hotfix/- - git push -u origin hotfix/- - ``` - -4. Confirm the branch was created: - ```bash - git branch --show-current - git log --oneline -3 - ``` - -## Branch Naming Rules - -- Pattern: `hotfix/AMRIT--` -- Slug: lowercase kebab-case, max 5 words -- Example: `hotfix/AMRIT-789-fix-jwt-expiry-crash` - -## Post-Merge Reminders - -After the hotfix PR merges to `main`, remind the user to: -1. Tag the release: `git tag -a vX.Y.Z+1 -m "Hotfix: "` -2. Back-merge `main` into the current `release-X.Y.Z` branch so the fix is not lost: - ```bash - git checkout release-X.Y.Z - git merge main - git push origin release-X.Y.Z - ``` - -## Notes - -- Hotfix branches MUST be cut from `main`, not from a release branch. -- Use this only for critical production issues. For non-urgent fixes use `/new-bugfix`. -- If the user has uncommitted changes, warn them and ask whether to stash before switching. diff --git a/.claude/commands/pr-checklist.md b/.claude/commands/pr-checklist.md deleted file mode 100644 index 0799185a..00000000 --- a/.claude/commands/pr-checklist.md +++ /dev/null @@ -1,167 +0,0 @@ -# /pr-checklist - -Audit the current branch against AMRIT PR standards before opening a pull request. Report every finding — do not skip items that cannot be auto-checked. - -## Execution Plan - -Run each check in order. For each check, print: `[PASS]`, `[FAIL]`, or `[WARN]` followed by a one-line explanation. - ---- - -### 1. Branch Name - -```bash -git branch --show-current -``` - -- PASS if: matches `feature/AMRIT-\d+-.+`, `bugfix/AMRIT-\d+-.+`, or `hotfix/AMRIT-\d+-.+` -- FAIL if: does not include a Jira ticket ID or uses `main`/`release-*` - ---- - -### 2. Target Branch - -```bash -git log --oneline origin/main..HEAD | wc -l -git branch -r | grep 'release-' | sort -V | tail -1 -``` - -- Confirm the PR targets the correct branch (feature/bugfix → latest release branch; hotfix → main). -- WARN if the branch appears to have diverged by more than 50 commits. - ---- - -### 3. Build Passes - -```bash -mvn clean package -Denv=example -q 2>&1 | tail -5 -``` - -- PASS if build exits 0. -- FAIL if there are compilation errors. - ---- - -### 4. Tests Pass - -```bash -mvn test -DskipTests=false 2>&1 | grep -E 'Tests run|BUILD|FAILURE|ERROR' | tail -20 -``` - -- PASS if `BUILD SUCCESS` and 0 failures/errors. -- FAIL if any test fails or the build errors. - ---- - -### 5. No Hardcoded Secrets - -```bash -git diff origin/main...HEAD -- '*.java' '*.properties' '*.yml' '*.yaml' | \ - grep -E '(?i)(password|secret|token|apikey|api_key)\s*=\s*["\x27][^$\{]' || echo "CLEAN" -``` - -- PASS if output is `CLEAN`. -- FAIL if any matches found — list the file and line. - ---- - -### 6. No System.out / printStackTrace - -```bash -git diff origin/main...HEAD -- '*.java' | grep -E '(System\.out\.|e\.printStackTrace)' | head -10 || echo "CLEAN" -``` - -- PASS if `CLEAN`. -- FAIL if matches found. - ---- - -### 7. No PII in Logs - -```bash -git diff origin/main...HEAD -- '*.java' | \ - grep -E 'log\.(info|debug|warn|error).*\b(name|aadhaar|phone|mobile|address|dob)\b' | head -10 || echo "CLEAN" -``` - -- PASS if `CLEAN`. -- WARN if matches found — manual review required. - ---- - -### 8. @Valid Present on Controller Endpoints - -```bash -git diff origin/main...HEAD -- 'src/main/java/**/controller/**/*.java' | \ - grep -E '(@PostMapping|@PutMapping|@PatchMapping)' -A3 | grep -v '@Valid' | head -20 || echo "CLEAN" -``` - -- WARN if any mutation endpoint parameter is missing `@Valid`. - ---- - -### 9. No Direct Repository Calls in Controllers - -```bash -git diff origin/main...HEAD -- 'src/main/java/**/controller/**/*.java' | \ - grep -E 'Repository|\.save\(|\.findBy' | head -10 || echo "CLEAN" -``` - -- FAIL if controllers import or call repositories directly. - ---- - -### 10. No JPA Entities Returned from Controllers - -```bash -git diff origin/main...HEAD -- 'src/main/java/**/controller/**/*.java' | \ - grep -E 'ResponseEntity<(List<)?(AncCare|DiseaseAESJE|CbacDetails|ChildRegister|[A-Z][a-z]+Entity)' | head -10 || echo "CLEAN" -``` - -- FAIL if an entity type appears as a controller return type. - ---- - -### 11. Tests Added for New Code - -```bash -git diff --name-only origin/main...HEAD | grep 'src/main/java' | sed 's|src/main/java/||;s|\.java$||' | \ - while read class; do - base=$(basename "$class") - find src/test -name "${base}Test.java" | head -1 - done -``` - -- WARN for every changed production class that has no corresponding test class. - ---- - -### 12. Constructor Injection Only - -```bash -git diff origin/main...HEAD -- '*.java' | grep -E '@Autowired\s+private' | head -10 || echo "CLEAN" -``` - -- FAIL if field-level `@Autowired` is used. - ---- - -## Summary - -After all checks, print a summary table: - -| # | Check | Status | -|---|---|---| -| 1 | Branch name | | -| 2 | Target branch | | -| 3 | Build passes | | -| 4 | Tests pass | | -| 5 | No hardcoded secrets | | -| 6 | No System.out | | -| 7 | No PII in logs | | -| 8 | @Valid on endpoints | | -| 9 | No repo calls in controller | | -| 10 | No entities from controller | | -| 11 | Tests added | | -| 12 | Constructor injection | | - -Then print: **"Ready to open PR"** if all checks PASS/WARN, or **"PR blocked — fix FAIL items first"** if any FAIL. diff --git a/.claude/skills/java-code-reviewer/SKILL.md b/.claude/skills/java-code-reviewer/SKILL.md deleted file mode 100644 index a342b18a..00000000 --- a/.claude/skills/java-code-reviewer/SKILL.md +++ /dev/null @@ -1,247 +0,0 @@ -# java-code-reviewer - -Orchestrator skill for reviewing Java / Spring Boot code in the AMRIT / FLW-API project. - -## Triggers - -Activate this skill when the user says any of: -- "review this", "check my code", "audit this", "PR review", "code review" -- "is this correct", "what's wrong with this", "look at this file" -- Pastes raw Java code or a file path to a `.java` file -- Shares a diff or lists 2+ changed files - ---- - -## Routing Decision - -### Single file or pasted snippet -Run a full inline review yourself. Work through all 6 dimensions below in order. Produce the structured report defined in **Report Format**. - -### PR with 3 or more changed files -Spawn the three specialized agents **in parallel**: -1. `agents/security-auditor.md` — security scan -2. `agents/architecture-reviewer.md` — layering and SOLID check -3. `agents/test-reviewer.md` — test coverage and quality - -Wait for all three JSON outputs. Then synthesize them into one final **Synthesized PR Report** (format defined below). Do not produce a partial report while agents are running. - ---- - -## 6 Review Dimensions - -Work through every dimension for every file. Skip a dimension only if it is genuinely not applicable (e.g., no tests exist yet and the task is not about testing). - -### 1. Architecture & Layering -Enforce the strict AMRIT layer contract: - -``` -Controller → Service Interface → Service Impl → Repository → Entity - ↕ - Mapper (DTO ↔ Entity) -``` - -Check: -- Controllers import only: DTOs, service interfaces, `ApiResponse`, Spring MVC types, validation annotations. No repository or entity imports. -- Service implementations own all business logic. No Spring MVC types (`HttpServletRequest`, etc.). -- Repositories are Spring Data JPA interfaces only. No business logic. -- DTOs cross the Controller ↔ Service boundary. Entities never leave the service layer. -- Mappers (MapStruct) handle all DTO ↔ Entity conversion. No manual field copying. -- Dependency direction never points upward (lower layer must not import higher layer). - -### 2. Security -This project uses **custom JWT auth** (not Spring Security method security). There are no `@PreAuthorize` annotations in the current codebase. The security model is: -- `JwtUserIdValidationFilter` validates the JWT and returns 401 if invalid -- `HTTPRequestInterceptor` validates the token exists in Redis via `Validator.checkKeyExists()` -- Role data lives in `v_userservicerolemapping` (read via `UserServiceRoleRepo`) - -Check for: -- Missing `Authorization` header requirement on controller endpoints (all non-public endpoints must declare `@RequestHeader("Authorization")` or have `headers = "Authorization"` on the `@RequestMapping`) -- Missing `@Valid` on any `@RequestBody` parameter -- `@RequestParam` / `@PathVariable` accepting IDs without `@Positive`/`@Min(1)` -- Hardcoded passwords, API keys, tokens, or connection strings in any `.java` or `.properties` file -- PII in log statements: names, Aadhaar, phone numbers, addresses, dates of birth -- `@Query` values built with string concatenation (SQL/JPQL injection) -- Sensitive fields stored without `CryptoUtil.encrypt()` / `CryptoUtil.decrypt()` -- `System.out.println` or `e.printStackTrace()` in production code - -### 3. Code Quality -- Field injection (`@Autowired` on fields) — must be constructor injection -- `System.out.println` or `e.printStackTrace()` instead of SLF4J logger -- Empty `catch` blocks (exception swallowing) -- Generic `RuntimeException` thrown instead of domain exceptions (`ResourceNotFoundException`, etc.) -- Overly long methods (> 50 lines) or deeply nested code (> 4 levels) -- Dead code, commented-out blocks -- Naming: classes PascalCase, methods/fields camelCase, constants SCREAMING_SNAKE - -### 4. Spring Boot Best Practices -- Write methods missing `@Transactional` -- Read methods missing `@Transactional(readOnly = true)` -- `@OneToMany` / `@ManyToMany` collections using `FetchType.EAGER` (default for `@ManyToMany` is EAGER — must be overridden to LAZY) -- `@OneToMany` without `@BatchSize` or `@EntityGraph` where collections are iterated -- Returning raw entity from a controller instead of a DTO -- `new` construction of Spring beans (bypasses the container) -- `@Value` used on non-`String` fields without type conversion (silent null) - -### 5. Testing -- Every public service method must have at least one test -- Tests must use JUnit 5 (`@ExtendWith(MockitoExtension.class)`) — not JUnit 4 `@RunWith` -- `@Mock` for dependencies, `@InjectMocks` for the class under test -- AAA pattern: Arrange / Act / Assert clearly separated by blank lines -- `@DisplayName` on test class and every test method -- Happy path + not-found path + exception path coverage per method -- Assertions must be specific — `assertNotNull` alone is a warning -- `verify()` to confirm side effects (saves, deletes) -- No `Thread.sleep`, no real PII in fixtures - -### 6. Performance -- N+1 queries: iterating a collection and calling a repo method per element -- Missing `@Transactional` on multi-step writes (separate commits on failure) -- `findAll()` on a table without a `WHERE` clause on large tables -- `String` concatenation in loops (use `StringBuilder`) -- Repeated `.getSessionObject()` calls inside loops instead of one call outside - ---- - -## Report Format — Single File - -``` -## Code Review — - -### Summary -<2–3 sentences: what the code does, overall quality verdict, biggest risk> - ---- - -### 🔴 Critical Issues - -**[ARCH-1] ** — `FileName.java:<line>` -> <What is wrong and why it matters> -> Fix: <specific action> - -*(None)* — if no criticals - ---- - -### 🟡 Warnings -**[SEC-1] <Title>** — `FileName.java:<line>` -> <What is wrong> -> Fix: <specific action> - -*(None)* — if no warnings - ---- - -### 🟢 Suggestions -**[QUAL-1] <Title>** — `FileName.java:<line>` -> <What could be improved> - -*(None)* — if no suggestions - ---- - -### ✅ What's Done Well -- <Specific correct pattern observed — e.g., "Constructor injection used throughout"> -- <max 3 items> - ---- - -### Verdict - -| Dimension | Status | -|----------------------|-------------------------| -| Architecture | ✅ / 🔴 N critical / 🟡 N warnings | -| Security | ✅ / 🔴 N critical / 🟡 N warnings | -| Code Quality | ✅ / 🟡 N warnings / 🟢 N suggestions | -| Spring Boot | ✅ / 🟡 N warnings | -| Testing | ✅ / 🟡 N warnings | -| Performance | ✅ / 🟢 N suggestions | - -**APPROVED** — all dimensions pass -**APPROVED WITH COMMENTS** — only warnings/suggestions, no criticals -**CHANGES REQUESTED** — one or more critical issues present -``` - ---- - -## Report Format — Synthesized PR Report (3+ files) - -After receiving JSON from all three agents, produce: - -``` -## PR Review — <branch or PR title> - -### Summary -<3–4 sentences: what the PR does, total finding counts, merge recommendation> - ---- - -### 🔴 Critical Issues (<N total>) -<Merge all CRITICAL findings from all three agent outputs.> -**[<ID>] <title>** — `<file>:<line>` -> <description> -> Fix: <recommendation> - ---- - -### 🟡 Warnings (<N total>) -<Merge all WARNING findings from all three agent outputs.> - ---- - -### 🟢 Suggestions (<N total>) -<Merge all INFO/SUGGESTION findings.> - ---- - -### Coverage Summary (from test-reviewer) -| Class | Methods | Fully Covered | Partial | Not Covered | -|---|---|---|---|---| -| <ClassName> | N | N | N | N | - -Overall coverage: X / Y methods fully tested (Z%) - ---- - -### Verdict - -| Dimension | Agent | Status | -|----------------|---------------------|----------------| -| Security | security-auditor | 🔴 / 🟡 / ✅ | -| Architecture | architecture-reviewer | 🔴 / 🟡 / ✅ | -| Testing | test-reviewer | 🟡 / ✅ | - -**APPROVED** / **APPROVED WITH COMMENTS** / **CHANGES REQUESTED** -``` - ---- - -## Finding ID Prefixes - -| Prefix | Dimension | -|---|---| -| `ARCH-n` | Architecture & Layering | -| `SEC-n` | Security | -| `QUAL-n` | Code Quality | -| `BOOT-n` | Spring Boot Best Practice | -| `TEST-n` | Testing | -| `PERF-n` | Performance | - ---- - -## Verdict Rules - -| Condition | Verdict | -|---|---| -| Zero criticals, zero warnings | APPROVED | -| Zero criticals, one or more warnings or suggestions | APPROVED WITH COMMENTS | -| One or more criticals | CHANGES REQUESTED | - ---- - -## Reference Files - -- `references/review-checklist.md` — per-class-type checklist -- `references/common-issues.md` — 10 AMRIT anti-patterns with wrong/correct examples -- `agents/security-auditor.md` — deep security scan agent -- `agents/architecture-reviewer.md` — layering and SOLID agent -- `agents/test-reviewer.md` — test coverage and quality agent diff --git a/.claude/skills/java-code-reviewer/agents/architecture-reviewer.md b/.claude/skills/java-code-reviewer/agents/architecture-reviewer.md deleted file mode 100644 index 99527896..00000000 --- a/.claude/skills/java-code-reviewer/agents/architecture-reviewer.md +++ /dev/null @@ -1,181 +0,0 @@ -# Architecture Reviewer Agent - -You are an architecture reviewer for the AMRIT / FLW-API Java Spring Boot project. Verify that code respects the AMRIT layered architecture, SOLID principles, and dependency direction rules. Return all findings as structured JSON only. - -## AMRIT Architecture Model - -The mandatory dependency direction is: - -``` -Controller → Service Interface → Service Impl → Repository → Entity - ↕ - Mapper (DTO ↔ Entity) -``` - -**Nothing in a lower layer imports from a higher layer.** - -### Layer Identification Rules - -Determine which layer a file belongs to from its package: - -| Package pattern | Layer | -|---|---| -| `com.iemr.flw.controller` | CONTROLLER | -| `com.iemr.flw.service` (interface) | SERVICE_INTERFACE | -| `com.iemr.flw.service.impl` | SERVICE_IMPL | -| `com.iemr.flw.repo.iemr` or `repo.identity` | REPOSITORY | -| `com.iemr.flw.domain.iemr` or `domain.identity` | ENTITY | -| `com.iemr.flw.dto` | DTO | -| `com.iemr.flw.mapper` | MAPPER | -| `com.iemr.flw.config` | CONFIG | -| `com.iemr.flw.utils` | UTIL | - ---- - -## Checks to Perform - -### CHECK-1: Business Logic in Controller -A controller method contains business logic if it has any of: -- `if/else` beyond a null-guard on the return value -- Arithmetic, date calculations, or status derivation -- Direct calls to a `@Repository` or `JpaRepository` bean -- A `new Entity()` construction with field assignment - -Flag: file, line number, the specific line(s) of logic found. - -Severity: **CRITICAL** - -### CHECK-2: Direct Repository Import or Call in Controller -If a controller class imports any class from `repo.iemr` or `repo.identity`, or calls a method on a repository bean, that is a direct layering violation. - -Severity: **CRITICAL** - -### CHECK-3: JPA Entity Returned from Controller -If a controller method's return type includes an entity class (i.e., a type from `domain.iemr` or `domain.identity`), or if a `ResponseEntity<EntityClass>` is declared, flag it. - -Severity: **CRITICAL** - -### CHECK-4: Field Injection Instead of Constructor Injection -Scan for `@Autowired` applied directly to a field (not a constructor or setter). This is the `@Autowired\n private SomeType field;` pattern. - -Also flag: `@Autowired` setter methods (warn, prefer constructor). - -Severity: **WARNING** - -### CHECK-5: Service Implementation Importing Spring MVC Types -A `service.impl` class that imports `jakarta.servlet.http.HttpServletRequest`, `jakarta.servlet.http.HttpServletResponse`, `org.springframework.web.context.request.WebRequest`, or `org.springframework.mock.web.*` has a dependency pointing upward toward the web layer. - -Severity: **WARNING** - -### CHECK-6: Entity Containing Business Logic -Flag any entity class that: -- Calls a Spring bean (would require `@Autowired` inside entity — also a separate violation) -- Implements a non-trivial `@PrePersist` / `@PreUpdate` lifecycle method (> 3 lines, contains conditionals) -- Has a method with business semantics (not just getters/setters/`equals`/`hashCode`/`toString`) - -Severity: **WARNING** - -### CHECK-7: SOLID — Single Responsibility -Flag any service implementation method that: -- Exceeds 50 lines -- Performs more than one distinct business operation (e.g., saves record AND sends notification AND updates another entity — should be decomposed) - -Report the method name and line count. - -Severity: **WARNING** - -### CHECK-8: SOLID — Interface Segregation -Flag any service interface with more than 15 declared methods. Large interfaces force implementors to implement methods they don't need and are a sign the interface should be split. - -Severity: **WARNING** - -### CHECK-9: SOLID — Dependency Inversion -Flag any class that instantiates a concrete service or repository implementation with `new`: -```java -SomeServiceImpl svc = new SomeServiceImpl(...); -``` -Spring-managed beans must be injected, not constructed inline. - -Severity: **WARNING** - -### CHECK-10: Mapper Calling a Service or Repository -A mapper class in `com.iemr.flw.mapper` must not import or invoke any service or repository bean. Mappers are pure transformation utilities. - -Severity: **WARNING** - -### CHECK-11: Wrong Package Placement -Flag files that exist in a package inconsistent with their role: -- A class annotated `@Service` in a `controller` package -- A class annotated `@RestController` in a `service` package -- A `JpaRepository` in a `service` or `controller` package - -Severity: **WARNING** - -### CHECK-12: Circular Service Dependencies -Flag if Service A injects Service B which injects Service A (detected from `@Autowired` / constructor signatures). Spring will fail to start with a circular dependency error. - -Severity: **CRITICAL** - ---- - -## Output Format - -Return **only** valid JSON. No prose before or after the JSON object. - -```json -{ - "auditTarget": "<filename(s) or PR title>", - "summary": { - "criticalCount": 0, - "warningCount": 0, - "infoCount": 0, - "totalChecks": 12, - "passedChecks": 0, - "layeringViolations": 0, - "solidViolations": 0 - }, - "findings": [ - { - "id": "ARCH-1", - "severity": "CRITICAL", - "check": "CHECK-1", - "category": "BUSINESS_LOGIC_IN_CONTROLLER", - "file": "com/iemr/flw/controller/CoupleController.java", - "line": 55, - "affectedLayer": "CONTROLLER", - "title": "Business logic in controller: age validation", - "description": "Lines 55–60 implement age validation (if husbandAge < 18) directly in the controller method saveEligibleCouple(). Controllers must delegate all validation beyond @Valid to the service layer.", - "codeSnippet": "if (request.getHusbandAge() < 18 || request.getWifeAge() < 18) {\n return ResponseEntity.badRequest()...\n}", - "recommendation": "Move the age validation to EligibleCoupleServiceImpl. The controller should call coupleService.save(request) and return the result." - } - ], - "passedChecks": [ - "CHECK-2: No direct repository imports found in any controller", - "CHECK-3: No entity types found in controller return signatures", - "CHECK-12: No circular service dependencies detected" - ] -} -``` - -### Severity Values -- `"CRITICAL"` — direct layering violation, circular dependency, entity exposed at API boundary -- `"WARNING"` — SOLID violation, wrong injection style, wrong package, upward dependency -- `"INFO"` — style or naming concern with no correctness impact - -### Category Values -`BUSINESS_LOGIC_IN_CONTROLLER`, `DIRECT_REPO_IN_CONTROLLER`, `ENTITY_FROM_CONTROLLER`, `FIELD_INJECTION`, `UPWARD_DEPENDENCY`, `BUSINESS_LOGIC_IN_ENTITY`, `SOLID_SINGLE_RESPONSIBILITY`, `SOLID_INTERFACE_SEGREGATION`, `SOLID_DEPENDENCY_INVERSION`, `MAPPER_CALLS_SERVICE`, `WRONG_PACKAGE`, `CIRCULAR_DEPENDENCY`, `OTHER` - -### affectedLayer Values -`CONTROLLER`, `SERVICE_INTERFACE`, `SERVICE_IMPL`, `REPOSITORY`, `ENTITY`, `MAPPER`, `DTO`, `CONFIG`, `UTIL` - ---- - -## Execution Instructions - -1. For each provided file, determine its layer from the package name using the table above. -2. Run all 12 checks applicable to that layer. -3. For each issue found: one entry in `findings[]` with a specific file path and line number. -4. For each check that passes cleanly across all files: one brief entry in `passedChecks[]`. -5. Never hallucinate — only report what can be pointed to in the actual code. -6. Count totals accurately in `summary`. -7. Return only the JSON object — no explanation, no markdown fences, no surrounding text. diff --git a/.claude/skills/java-code-reviewer/agents/security-auditor.md b/.claude/skills/java-code-reviewer/agents/security-auditor.md deleted file mode 100644 index 8d5e52ee..00000000 --- a/.claude/skills/java-code-reviewer/agents/security-auditor.md +++ /dev/null @@ -1,145 +0,0 @@ -# Security Auditor Agent - -You are a security auditor for the AMRIT / FLW-API Java Spring Boot project. Perform a deep security scan of the provided code and return findings as structured JSON only. - -## Project Security Context - -This project does NOT use Spring Security method-level annotations (`@PreAuthorize`, `@Secured`). The current auth model is: -- `JwtUserIdValidationFilter` — validates JWT from cookie (`Jwttoken`) or header (`JwtToken`); returns 401 if missing/invalid -- `HTTPRequestInterceptor` — validates the `Authorization` token exists in Redis via `Validator.checkKeyExists()` -- Skipped paths: `/user/userAuthenticate`, `/swagger-ui/**`, `/v3/api-docs/**`, `/public/**`, `/health`, `/version` -- Role data lives in `v_userservicerolemapping` via `UserServiceRoleRepo` — but role enforcement is **not** currently wired into the request pipeline - -Evaluate security relative to this actual model — do not flag missing `@PreAuthorize` as a finding unless Spring Security has been added. DO flag endpoints that lack `@RequestMapping(headers = "Authorization")` or `@RequestHeader("Authorization")`, as that bypasses the interceptor chain. - ---- - -## Checks to Perform - -### CHECK-1: Missing Authorization Header Declaration -Every controller that handles protected data must declare the authorization requirement at the class or method level. Flag endpoints where: -- The `@RequestMapping` on the class does not include `headers = "Authorization"` -- AND the method does not declare `@RequestHeader("Authorization") String Authorization` -- AND the path is not in the public skip list - -Severity: **CRITICAL** - -### CHECK-2: Missing @Valid on @RequestBody -Every mutation endpoint (`@PostMapping`, `@PutMapping`, `@PatchMapping`) that accepts a `@RequestBody` must have `@Valid` on that parameter. Without it, all Bean Validation annotations on the DTO are silently bypassed. - -Severity: **CRITICAL** - -### CHECK-3: Path Variables / Request Params Without Constraints -`@PathVariable` and `@RequestParam` values accepting IDs must use `@Positive` or `@Min(1)` to prevent negative/zero IDs from reaching the DB. - -Severity: **WARNING** - -### CHECK-4: Hardcoded Credentials, Tokens, or Keys -Scan for: -- `password`, `secret`, `token`, `apiKey`, `api_key`, `key` assigned a string literal (not `${...}`) -- Long Base64-looking strings (> 20 chars) assigned to a field -- Connection strings with embedded credentials -- Property files committing real values for `password`, `token`, `secret` keys (not `<placeholder>` or `${ENV_VAR}`) - -Severity: **CRITICAL** - -### CHECK-5: PII or Health Data in Log Statements -Scan every `log.info`, `log.debug`, `log.warn`, `log.error`, `logger.info`, `logger.debug`, `logger.warn`, `logger.error`, and `System.out.println` statement for: -- Field names or variables suggesting PII: `name`, `aadhaarNo`, `aadhaar`, `phoneNo`, `phone`, `mobile`, `address`, `dob`, `dateOfBirth`, `motherName`, `fatherName` -- Health data: `diagnosis`, `tbResult`, `hivStatus`, `bloodGroup` - -Flag the log statement, the field/variable name referenced, and the file+line. - -Severity: **CRITICAL** (health data in a patient-facing system is a regulatory breach) - -### CHECK-6: SQL Injection via String Concatenation -Scan `@Query` annotation values for `+` concatenation or `String.format()` used to build the query string. Also scan for `EntityManager.createNativeQuery(string)` where `string` contains a variable. - -Severity: **CRITICAL** - -### CHECK-7: Plain Text Password Storage -Scan for `passwordEncoder` absent where user passwords are being set/stored. Also flag MD5, SHA-1, SHA-256 (without bcrypt) used as password hashing. - -Severity: **CRITICAL** - -### CHECK-8: Sensitive Fields Stored Without Encryption -In service implementations: flag any `.set<SensitiveField>(rawValue)` where the field name suggests sensitivity (`aadhaarNo`, `phoneNo`, `address`) and the value is not wrapped in `cryptoUtil.encrypt(...)` or `CryptoUtil.encrypt(...)`. - -Severity: **WARNING** (would be CRITICAL if CryptoUtil usage is established project standard) - -### CHECK-9: System.out / printStackTrace in Production Code -Flag any `System.out.println`, `System.err.println`, or `e.printStackTrace()` in files under `src/main/`. - -Severity: **WARNING** - -### CHECK-10: Sensitive Data in Response DTOs -Flag controller methods that return types or response DTOs containing fields like `aadhaarNo`, `password`, `rawPassword`, `secretKey`, or similar — fields that should never be serialized to an API response. - -Severity: **CRITICAL** - ---- - -## Output Format - -Return **only** valid JSON. No prose before or after the JSON object. - -```json -{ - "auditTarget": "<filename(s) or PR title>", - "summary": { - "criticalCount": 0, - "warningCount": 0, - "infoCount": 0, - "totalChecks": 10, - "passedChecks": 0 - }, - "findings": [ - { - "id": "SEC-1", - "severity": "CRITICAL", - "check": "CHECK-2", - "category": "INPUT_VALIDATION", - "file": "com/iemr/flw/controller/CbacController.java", - "line": 34, - "title": "Missing @Valid on @RequestBody", - "description": "The POST /saveCbacDetails endpoint accepts CbacDetailsRequest without @Valid. Bean Validation annotations on the DTO are not enforced. A null beneficiaryId will cause a NullPointerException in the service layer.", - "codeSnippet": "@PostMapping(\"/saveCbacDetails\")\npublic ResponseEntity<?> saveCbacDetails(@RequestBody CbacDetailsRequest request) {", - "recommendation": "Change to: @Valid @RequestBody CbacDetailsRequest request", - "owaspCategory": "A04:2021 Insecure Design" - } - ], - "passedChecks": [ - "CHECK-1: All controllers declare headers = \"Authorization\" at class level", - "CHECK-6: No string concatenation found in @Query values" - ] -} -``` - -### Severity Values -- `"CRITICAL"` — exploitable, data breach risk, or regulatory violation -- `"WARNING"` — weakens security posture, violates project standards -- `"INFO"` — informational observation, no immediate risk - -### Category Values -`MISSING_AUTH_HEADER`, `INPUT_VALIDATION`, `HARDCODED_SECRET`, `PII_LOGGING`, `SQL_INJECTION`, `WEAK_CRYPTO`, `PLAIN_TEXT_PASSWORD`, `MISSING_ENCRYPTION`, `SENSITIVE_EXPOSURE`, `DEBUG_OUTPUT`, `OTHER` - -### OWASP Top 10 2021 Reference -- A01: Broken Access Control → CHECK-1 -- A02: Cryptographic Failures → CHECK-7, CHECK-8 -- A03: Injection → CHECK-6 -- A04: Insecure Design → CHECK-2, CHECK-3, CHECK-10 -- A05: Security Misconfiguration → CHECK-9 -- A07: Identification and Authentication Failures → CHECK-1 -- A09: Security Logging and Monitoring Failures → CHECK-5 - ---- - -## Execution Instructions - -1. Read every `.java` file provided. -2. Run all 10 checks against every file. -3. For each issue found: one entry in `findings[]` with specific file path and line number. -4. For each check that passes cleanly on all files: one entry in `passedChecks[]`. -5. Never hallucinate a finding — only report what can be pointed to in the actual code. -6. Count totals accurately in `summary`. -7. Return only the JSON object — no explanation, no markdown fences. diff --git a/.claude/skills/java-code-reviewer/agents/test-reviewer.md b/.claude/skills/java-code-reviewer/agents/test-reviewer.md deleted file mode 100644 index 5fca8cbe..00000000 --- a/.claude/skills/java-code-reviewer/agents/test-reviewer.md +++ /dev/null @@ -1,226 +0,0 @@ -# Test Reviewer Agent - -You are a test quality reviewer for the AMRIT / FLW-API Java Spring Boot project. Map test coverage per service method, assess test quality across multiple dimensions, and return findings as structured JSON only. - -## Project Test Context - -- Framework: **JUnit 5** (`@ExtendWith(MockitoExtension.class)`) — not JUnit 4 -- Mocking: **Mockito** (`@Mock`, `@InjectMocks`) -- Pattern: **AAA** — Arrange / Act / Assert -- Style: `@DisplayName` on every class and method describing behavior -- Test class naming convention: `<ProductionClass>Test` in the mirrored package under `src/test/` - ---- - -## Phase 1: Coverage Mapping - -For every **public method** in each changed service implementation (classes in `service.impl`): - -1. Locate the corresponding test class: same class name + `Test` suffix in `src/test/`. -2. For each production method, scan all `@Test` methods in the test class to determine which ones exercise it (the method name or a `when(...)` stub for it appears in the test body). -3. Classify coverage: - -| Coverage | Definition | -|---|---| -| `COVERED_FULL` | Happy-path test + at least one failure/not-found test + at least one exception test all present | -| `HAPPY_PATH_ONLY` | At least one test calls this method but no failure/exception path is tested | -| `MISSING` | No test method calls this production method at all | - -4. If no test class exists for a production class, mark **all** its public methods as `MISSING`. - ---- - -## Phase 2: Quality Checks - -For every `@Test` method in the test class, run these checks: - -### CHECK-T1: JUnit Version -`@ExtendWith(MockitoExtension.class)` must be present on the test class. Flag `@RunWith(MockitoJUnitRunner.class)` or `@RunWith(SpringRunner.class)` as JUnit 4 usage. - -Severity: **WARNING** - -### CHECK-T2: @DisplayName on Test Class -The class must have `@DisplayName("...")`. Content should describe the class under test in plain English, e.g. `"AncServiceImpl"` or `"ANC Care Service"`. - -Severity: **INFO** - -### CHECK-T3: @DisplayName on Every Test Method -Each `@Test` method must have `@DisplayName("should <behaviour> when <condition>")`. Flag any method missing it or using only the method name pattern like `testSaveAnc()`. - -Severity: **WARNING** - -### CHECK-T4: AAA Pattern -Each test method should have exactly three logical sections: -- **Arrange**: mock setup (`when(...).thenReturn(...)`, test data creation) -- **Act**: single call to the method under test -- **Assert**: assertion(s) on the result and/or `verify(...)` calls - -Flag tests where: -- Assertions appear before the Act call -- Multiple Act calls exist in one test (likely testing two things) -- Arrange and Assert are interleaved with no clear separation - -Severity: **WARNING** - -### CHECK-T5: Mock Setup -- All dependencies injected into the class under test must be declared with `@Mock` -- The class under test must be declared with `@InjectMocks` -- Flag: `Mockito.mock(SomeClass.class)` called inside a test body (not a field) when `@Mock` would suffice -- Flag: `@Spy` used where `@Mock` is appropriate (and no partial mock is needed) - -Severity: **WARNING** - -### CHECK-T6: Failure Path Coverage -For every public method that calls `orElseThrow(...)` or has a `findBy...` repository call, check that there is a test exercising the path where the repository returns `Optional.empty()` or an empty list. - -The test should use `assertThrows(ResourceNotFoundException.class, () -> ...)`. - -Severity: **WARNING** - -### CHECK-T7: Exception Message Assertion -When `assertThrows` is used, the test should also assert the exception message: -```java -Exception ex = assertThrows(ResourceNotFoundException.class, () -> service.getById(99L)); -assertThat(ex.getMessage()).contains("99"); -``` - -Flag tests that only assert the exception type without checking the message. - -Severity: **INFO** - -### CHECK-T8: Weak Assertions -Flag test methods where the only assertion is: -- `assertNotNull(result)` — passes even if result has all null fields -- `assertTrue(result != null)` — same problem -- `assertEquals(result, result)` — tautology - -These must be supplemented with field-level assertions (`assertEquals(expected, result.getField())`). - -Severity: **WARNING** - -### CHECK-T9: Side Effect Verification -When the production method calls `repository.save(...)`, `repository.delete(...)`, or any void method with a side effect, the test should have a `verify(mockRepo).save(any())` or similar call. Flag tests that call a write method but never `verify` it. - -Severity: **WARNING** - -### CHECK-T10: Thread.sleep -Any `Thread.sleep(...)` in a test is an anti-pattern — it makes tests slow and flaky. Flag it. - -Severity: **CRITICAL** - -### CHECK-T11: Real PII in Fixtures -Scan string literals in test files for: -- Patterns matching 12-digit Aadhaar numbers (e.g. `"123456789012"`) -- Indian phone number patterns (`"9876543210"`, `"+919876543210"`) -- Strings that look like real person names combined with health data - -Severity: **CRITICAL** - -### CHECK-T12: Real Database or Redis Access -Flag `@SpringBootTest`, `@DataJpaTest` without `replace = Replace.ANY`, or any test that does not mock its repository dependencies (i.e., uses real `JpaRepository` calls). Service unit tests must not touch the DB. - -Severity: **CRITICAL** - ---- - -## Output Format - -Return **only** valid JSON. No prose before or after the JSON object. - -```json -{ - "auditTarget": "<filename(s) or PR title>", - "coverageSummary": { - "totalProductionMethods": 0, - "fullyCovered": 0, - "happyPathOnly": 0, - "missing": 0, - "coveragePercent": 0.0 - }, - "coverageMap": [ - { - "productionClass": "com.iemr.flw.service.impl.AncServiceImpl", - "method": "saveAncCare(AncCareRequest)", - "coverage": "COVERED_FULL", - "testClass": "com.iemr.flw.service.impl.AncServiceImplTest", - "testMethods": [ - "should save ANC record and return mapped response", - "should throw ResourceNotFoundException when beneficiary not found", - "should throw InvalidOperationException when gestational age is out of range" - ] - }, - { - "productionClass": "com.iemr.flw.service.impl.AncServiceImpl", - "method": "getAllByBeneficiary(Long)", - "coverage": "MISSING", - "testClass": "com.iemr.flw.service.impl.AncServiceImplTest", - "testMethods": [] - } - ], - "qualityFindings": [ - { - "id": "TEST-1", - "severity": "WARNING", - "check": "CHECK-T3", - "category": "MISSING_DISPLAY_NAME", - "file": "com/iemr/flw/service/impl/AncServiceImplTest.java", - "line": 45, - "title": "Test method missing @DisplayName", - "description": "Method testSaveAnc() at line 45 has no @DisplayName annotation. Describe the expected behaviour, e.g. 'should save ANC record and return mapped response'.", - "recommendation": "Add @DisplayName(\"should save ANC record and return mapped response when beneficiary exists\")" - }, - { - "id": "TEST-2", - "severity": "WARNING", - "check": "CHECK-T8", - "category": "WEAK_ASSERTION", - "file": "com/iemr/flw/service/impl/AncServiceImplTest.java", - "line": 62, - "title": "Assertion too weak — assertNotNull only", - "description": "The test at line 62 only calls assertNotNull(result). This passes even if the returned AncResponse has null beneficiaryId or null visitDate. Add field-level assertions.", - "recommendation": "Add: assertEquals(request.getBeneficiaryId(), result.getBeneficiaryId()); assertEquals(request.getVisitDate(), result.getVisitDate());" - } - ], - "qualitySummary": { - "totalTestMethods": 0, - "criticalIssues": 0, - "warningIssues": 0, - "infoIssues": 0, - "passedChecks": [] - } -} -``` - -### Severity Values -- `"CRITICAL"` — test uses real PII, accesses real DB/Redis, or `Thread.sleep` present -- `"WARNING"` — missing coverage path, weak assertions, JUnit 4 usage, no `verify()` on side effects -- `"INFO"` — `@DisplayName` missing, minor style issue - -### Coverage Values -`"COVERED_FULL"`, `"HAPPY_PATH_ONLY"`, `"MISSING"` - -### Category Values -`MISSING_DISPLAY_NAME`, `POOR_ASSERTION`, `WEAK_ASSERTION`, `MISSING_FAILURE_PATH`, `MISSING_EXCEPTION_ASSERTION`, `WRONG_MOCK_SETUP`, `JUNIT4_ANNOTATION`, `THREAD_SLEEP`, `PRODUCTION_DATA_IN_FIXTURE`, `MISSING_VERIFY`, `REAL_DB_ACCESS`, `AAA_VIOLATION`, `OTHER` - ---- - -## coveragePercent Formula - -``` -coveragePercent = (fullyCovered / totalProductionMethods) * 100 -``` - -Round to one decimal place. If `totalProductionMethods` is 0, set `coveragePercent` to 100.0. - ---- - -## Execution Instructions - -1. Read every changed production service implementation file (`service.impl`). -2. List all public methods (exclude `private`, `protected`, `static` helper methods). -3. Read the corresponding test file (class name + `Test`). If not found, mark all methods `MISSING`. -4. For each production method, scan all `@Test` methods to determine coverage category. -5. For each `@Test` method, run all 12 quality checks. -6. Compute coverage summary counts. -7. For quality checks that pass cleanly across all tests, add a brief entry to `passedChecks[]`. -8. Return only the JSON object — no explanation, no markdown fences, no surrounding text. diff --git a/.claude/skills/java-code-reviewer/references/common-issues.md b/.claude/skills/java-code-reviewer/references/common-issues.md deleted file mode 100644 index 5ab9898b..00000000 --- a/.claude/skills/java-code-reviewer/references/common-issues.md +++ /dev/null @@ -1,397 +0,0 @@ -# 10 Common AMRIT Anti-Patterns - -Each pattern shows the wrong code found in the wild, the correct replacement, and the rule it violates. - ---- - -## 1. Business Logic in Controller - -**Rule:** Controllers are thin. They validate input, call one service method, and return `ApiResponse`. All `if/else`, calculations, and DB access belong in the service. - -**Wrong:** -```java -@PostMapping("/saveEligibleCouple") -public ResponseEntity<?> saveEligibleCouple( - @RequestBody EligibleCoupleRequest request, - @RequestHeader("Authorization") String auth) { - - // Business logic directly in controller - if (request.getHusbandAge() < 18 || request.getWifeAge() < 18) { - return ResponseEntity.badRequest() - .body(new ApiResponse(false, "Age below minimum", null)); - } - EligibleCoupleRegister entity = new EligibleCoupleRegister(); - entity.setBeneficiaryId(request.getBeneficiaryId()); - entity.setHusbandAge(request.getHusbandAge()); - coupleRepo.save(entity); // direct repo call - return ResponseEntity.ok(new ApiResponse(true, "Saved", null)); -} -``` - -**Correct:** -```java -@PostMapping("/saveEligibleCouple") -public ResponseEntity<ApiResponse> saveEligibleCouple( - @Valid @RequestBody EligibleCoupleRequest request, - @RequestHeader("Authorization") String Authorization) { - try { - EligibleCoupleResponse saved = coupleService.saveEligibleCouple(request); - return ResponseEntity.ok(new ApiResponse(true, "Saved successfully", saved)); - } catch (Exception e) { - logger.error("Error saving eligible couple: {}", e.getMessage(), e); - return ResponseEntity.internalServerError() - .body(new ApiResponse(false, e.getMessage(), null)); - } -} -``` - ---- - -## 2. Field Injection Instead of Constructor Injection - -**Rule:** Use constructor injection. Field injection hides dependencies, prevents immutability, and breaks unit testing without a Spring context. - -**Wrong:** -```java -@Service -public class MaternalHealthServiceImpl implements MaternalHealthService { - - @Autowired - private AncCareRepo ancCareRepo; // field injection - - @Autowired - private BeneficiaryRepo beneficiaryRepo; - - @Autowired - private AncMapper ancMapper; -} -``` - -**Correct:** -```java -@Service -public class MaternalHealthServiceImpl implements MaternalHealthService { - - private final AncCareRepo ancCareRepo; - private final BeneficiaryRepo beneficiaryRepo; - private final AncMapper ancMapper; - - public MaternalHealthServiceImpl(AncCareRepo ancCareRepo, - BeneficiaryRepo beneficiaryRepo, - AncMapper ancMapper) { - this.ancCareRepo = ancCareRepo; - this.beneficiaryRepo = beneficiaryRepo; - this.ancMapper = ancMapper; - } -} -``` - -Or with Lombok: -```java -@Service -@RequiredArgsConstructor -public class MaternalHealthServiceImpl implements MaternalHealthService { - private final AncCareRepo ancCareRepo; - private final BeneficiaryRepo beneficiaryRepo; - private final AncMapper ancMapper; -} -``` - ---- - -## 3. Logging PII or Health Data - -**Rule:** Never log personally identifiable information or health data. Log entity IDs and status codes only. This is a regulatory requirement under India's DPDP Act and AMRIT's data governance policy. - -**Wrong:** -```java -logger.info("Saving ANC for beneficiary name={}, aadhaar={}, phone={}, dob={}", - beneficiary.getName(), - beneficiary.getAadhaarNo(), - beneficiary.getPhoneNo(), - beneficiary.getDob()); - -logger.debug("TB diagnosis: patient={}, result={}, testDate={}", - patient.getName(), diagnosisResult, testDate); -``` - -**Correct:** -```java -logger.debug("Saving ANC record for benefId={}", request.getBeneficiaryId()); - -logger.debug("TB diagnosis saved for benefId={}, recordId={}", - beneficiaryId, savedRecord.getId()); -``` - ---- - -## 4. Missing @Valid on Request Body - -**Rule:** Every `@RequestBody` parameter in a mutation endpoint must have `@Valid`. Without it, Bean Validation annotations on the DTO are silently ignored, allowing null/invalid data into the service layer. - -**Wrong:** -```java -@PostMapping("/saveCbacDetails") -public ResponseEntity<?> saveCbacDetails( - @RequestBody CbacDetailsRequest request, // no @Valid - @RequestHeader("Authorization") String auth) { - // request.getBeneficiaryId() could be null — NullPointerException downstream - return ResponseEntity.ok(cbacService.save(request)); -} -``` - -**Correct:** -```java -@PostMapping("/saveCbacDetails") -public ResponseEntity<ApiResponse> saveCbacDetails( - @Valid @RequestBody CbacDetailsRequest request, - @RequestHeader("Authorization") String Authorization) { - try { - return ResponseEntity.ok(new ApiResponse(true, "Saved", cbacService.save(request))); - } catch (Exception e) { - logger.error("Error saving CBAC details: {}", e.getMessage(), e); - return ResponseEntity.internalServerError() - .body(new ApiResponse(false, e.getMessage(), null)); - } -} -``` - -And the DTO must declare constraints: -```java -public class CbacDetailsRequest { - @NotNull(message = "beneficiaryId is required") - @Positive - private Long beneficiaryId; - - @NotNull(message = "providerServiceMapId is required") - private Integer providerServiceMapId; -} -``` - ---- - -## 5. Swallowing Exceptions Silently - -**Rule:** An empty `catch` block is always a bug. At minimum, log and re-throw. Prefer wrapping in a domain exception. - -**Wrong:** -```java -public String saveDeliveryOutcome(DeliveryOutcomeRequest request) { - try { - DeliveryOutcome entity = mapper.toEntity(request); - deliveryRepo.save(entity); - return "Saved"; - } catch (Exception e) { - // silent — caller never knows it failed - } - return null; -} -``` - -**Correct:** -```java -public DeliveryOutcomeResponse saveDeliveryOutcome(DeliveryOutcomeRequest request) { - try { - DeliveryOutcome entity = mapper.toEntity(request); - DeliveryOutcome saved = deliveryRepo.save(entity); - return mapper.toResponse(saved); - } catch (DataIntegrityViolationException e) { - logger.error("Duplicate delivery outcome for benefId={}", request.getBeneficiaryId(), e); - throw new DuplicateResourceException("DeliveryOutcome", "beneficiaryId", - request.getBeneficiaryId()); - } catch (Exception e) { - logger.error("Unexpected error saving delivery outcome for benefId={}", - request.getBeneficiaryId(), e); - throw new RuntimeException("Failed to save delivery outcome", e); - } -} -``` - ---- - -## 6. Returning Raw Entity from Controller - -**Rule:** JPA entities must never leave the service layer. Returning an entity from a controller exposes the DB schema, leaks lazy-loaded collections as serialization errors, and may expose sensitive fields. - -**Wrong:** -```java -@GetMapping("/getBeneficiary/{id}") -public ResponseEntity<?> getBeneficiary(@PathVariable Long id) { - BenRegDetail entity = benRepo.findById(id).orElse(null); - return ResponseEntity.ok(entity); // raw JPA entity in response -} -``` - -**Correct:** -```java -@GetMapping("/getBeneficiary/{id}") -public ResponseEntity<ApiResponse> getBeneficiary( - @PathVariable @Positive Long id, - @RequestHeader("Authorization") String Authorization) { - try { - BeneficiaryResponse response = beneficiaryService.getById(id); - return ResponseEntity.ok(new ApiResponse(true, "Fetched", response)); - } catch (Exception e) { - logger.error("Error fetching beneficiary id={}: {}", id, e.getMessage(), e); - return ResponseEntity.internalServerError() - .body(new ApiResponse(false, e.getMessage(), null)); - } -} -``` - ---- - -## 7. N+1 Query Problem - -**Rule:** Never call a repository method inside a loop over a collection. This fires one query per element (N+1 total). Use a `JOIN FETCH` or a batch query instead. - -**Wrong:** -```java -// SupervisorDashboardServiceImpl — N+1 pattern found in production -List<IncentiveActivityRecord> records = incentiveRepo.findByDistrictId(districtId); -for (IncentiveActivityRecord record : records) { - // One DB query per record to get the verifier's name - String verifierName = userServiceRoleRepo - .getUserRole(record.getVerifiedByUserId()) - .get(0) - .getName(); - record.setVerifiedByName(verifierName); -} -``` - -**Correct:** -```java -// Option 1: fetch all needed user IDs in one batch query -List<Integer> userIds = records.stream() - .map(IncentiveActivityRecord::getVerifiedByUserId) - .distinct() - .collect(Collectors.toList()); - -Map<Integer, String> userNameMap = userServiceRoleRepo.getUserNamesByIds(userIds) - .stream() - .collect(Collectors.toMap(UserServiceRoleDTO::getUserId, UserServiceRoleDTO::getName)); - -records.forEach(r -> r.setVerifiedByName(userNameMap.get(r.getVerifiedByUserId()))); - -// Option 2: use a JOIN in the original query to fetch the name alongside the record -@Query("SELECT new com.iemr.flw.dto.IncentiveWithVerifierDTO(i, u.name) " + - "FROM IncentiveActivityRecord i JOIN UserServiceRole u ON u.userId = i.verifiedByUserId " + - "WHERE i.districtId = :districtId AND u.userServciceRoleDeleted = false") -List<IncentiveWithVerifierDTO> findWithVerifierByDistrict(@Param("districtId") Integer districtId); -``` - ---- - -## 8. Missing @Transactional on Write Methods - -**Rule:** Any service method that performs two or more DB writes must be `@Transactional`. Without it, if the second write fails, the first is already committed — leaving data in a corrupted partial state. - -**Wrong:** -```java -public String saveAncAndUpdateBeneficiary(AncRequest request) { - // Two writes — no transaction. If second fails, first is committed. - ancCareRepo.save(ancMapper.toEntity(request)); - beneficiaryRepo.updateAncStatus(request.getBeneficiaryId(), "ANC_REGISTERED"); - return "Saved"; -} -``` - -**Correct:** -```java -@Transactional -public AncResponse saveAncAndUpdateBeneficiary(AncRequest request) { - AncCare saved = ancCareRepo.save(ancMapper.toEntity(request)); - beneficiaryRepo.updateAncStatus(request.getBeneficiaryId(), "ANC_REGISTERED"); - // Both roll back atomically if either fails - return ancMapper.toResponse(saved); -} -``` - -Also apply `@Transactional(readOnly = true)` to pure read methods: -```java -@Transactional(readOnly = true) -public List<AncResponse> getAllByBeneficiary(Long beneficiaryId) { - return ancCareRepo.findByBeneficiaryIdAndDeletedFalse(beneficiaryId) - .stream().map(ancMapper::toResponse).collect(Collectors.toList()); -} -``` - ---- - -## 9. Hardcoded Secrets or Credentials - -**Rule:** No credentials, API keys, tokens, or connection strings in source code. Use `@Value` referencing property keys that themselves reference environment variables in production. - -**Wrong:** -```java -private static final String TM_API_KEY = "amrit-tm-key-20240101"; -private static final String FHIR_TOKEN = "Bearer eyJhbGciOiJSUzI1NiJ9..."; - -// In properties file committed to repo: -// spring.datasource.password=Admin@123 -// fhir.api.token=Bearer eyJhbGciOiJSUzI1NiJ9... -``` - -**Correct:** -```java -@Value("${tm.api.key}") -private String tmApiKey; - -@Value("${fhir.api.token}") -private String fhirToken; -``` - -In `common_example.properties` (the committed template): -```properties -tm.api.key=<set-in-environment> -fhir.api.token=<set-in-environment> -spring.datasource.password=<set-in-environment> -``` - -In production deployment, values come from environment variables or a secrets manager — never from committed property files. - ---- - -## 10. Throwing Generic RuntimeException Instead of Custom Exceptions - -**Rule:** Use domain exceptions that communicate intent and map to the correct HTTP status. Raw `RuntimeException` gives a generic 500 to every caller regardless of the actual problem. - -**Wrong:** -```java -public AncResponse getAncById(Long id) { - AncCare record = ancCareRepo.findById(id) - .orElseThrow(() -> new RuntimeException("Not found")); // maps to 500 - return ancMapper.toResponse(record); -} - -public void validateAge(int age) { - if (age < 10 || age > 60) { - throw new RuntimeException("Invalid age"); // generic — no HTTP mapping - } -} -``` - -**Correct:** -```java -public AncResponse getAncById(Long id) { - AncCare record = ancCareRepo.findByIdAndDeletedFalse(id) - .orElseThrow(() -> new ResourceNotFoundException("AncCare", "id", id)); // 404 - return ancMapper.toResponse(record); -} - -public void validateAge(int age) { - if (age < 10 || age > 60) { - throw new InvalidOperationException( - "Age must be between 10 and 60, received: " + age); // 422 - } -} -``` - -Custom exception hierarchy: -``` -ResourceNotFoundException → 404 Not Found -DuplicateResourceException → 409 Conflict -InvalidOperationException → 422 Unprocessable Entity -``` - -All handled centrally by `GlobalExceptionHandler` — controllers never catch these. diff --git a/.claude/skills/java-code-reviewer/references/review-checklist.md b/.claude/skills/java-code-reviewer/references/review-checklist.md deleted file mode 100644 index 3ab4fbcf..00000000 --- a/.claude/skills/java-code-reviewer/references/review-checklist.md +++ /dev/null @@ -1,157 +0,0 @@ -# Per-Class Review Checklist — AMRIT / FLW-API - -Use this checklist when reviewing each file. Mark each item: ✅ PASS / ❌ FAIL / ➖ N/A - ---- - -## Controller - -> Package: `com.iemr.flw.controller` -> Naming: `<Domain>Controller` - -| # | Item | Severity if Failed | -|---|---|---| -| C-01 | Class annotated `@RestController` and `@RequestMapping(value = "/<path>", headers = "Authorization")` | 🟡 | -| C-02 | All dependencies injected via constructor — no `@Autowired` on fields | 🟡 | -| C-03 | Every method that accepts `@RequestBody` has `@Valid` on that parameter | 🔴 | -| C-04 | Every `@PathVariable` / `@RequestParam` accepting an ID has `@Positive` or `@Min(1)` | 🟡 | -| C-05 | Every method declares `@RequestHeader("Authorization") String Authorization` | 🟡 | -| C-06 | **Zero business logic** in the method body — at most: call one service method, return `ApiResponse` | 🔴 | -| C-07 | No direct repository imports or calls | 🔴 | -| C-08 | No JPA entity type in return signature — only DTOs or `ApiResponse<DTO>` | 🔴 | -| C-09 | Returns `ResponseEntity<ApiResponse<T>>` consistently (not `ResponseEntity<?>` or raw type) | 🟡 | -| C-10 | HTTP method semantics: `@PostMapping` for writes, `@GetMapping` for reads | 🟡 | -| C-11 | SLF4J logger: `private final Logger logger = LoggerFactory.getLogger(...)` | 🟡 | -| C-12 | No `System.out.println` or `e.printStackTrace()` | 🟡 | -| C-13 | No PII logged (no name, Aadhaar, phone, address in log statements) | 🔴 | -| C-14 | `catch` block logs the exception with `logger.error(...)` and returns an error `ApiResponse` — not empty | 🔴 | -| C-15 | `@Operation(summary = "...")` present for each endpoint (Swagger) | 🟢 | -| C-16 | No hardcoded String values used as role names or status codes — use constants | 🟢 | - ---- - -## Service Interface - -> Package: `com.iemr.flw.service` -> Naming: `<Domain>Service` - -| # | Item | Severity if Failed | -|---|---|---| -| S-01 | File is an `interface`, not a class | 🔴 | -| S-02 | Lives in `service/` package (not `service/impl/`) | 🟡 | -| S-03 | Name is `<Domain>Service` — not `I<Domain>Service` or `<Domain>ServiceInterface` | 🟢 | -| S-04 | All method parameters and return types are DTOs — never JPA entities | 🔴 | -| S-05 | No Spring annotations on the interface (`@Service`, `@Transactional`, etc.) | 🟡 | -| S-06 | No `throws Exception` — declare specific checked exceptions if any | 🟢 | - ---- - -## Service Implementation - -> Package: `com.iemr.flw.service.impl` -> Naming: `<Domain>ServiceImpl` - -| # | Item | Severity if Failed | -|---|---|---| -| SI-01 | Annotated `@Service` | 🟡 | -| SI-02 | Lives in `service/impl/` package | 🟡 | -| SI-03 | Declares `implements <Domain>Service` | 🔴 | -| SI-04 | All dependencies injected via constructor — no `@Autowired` on fields | 🟡 | -| SI-05 | Every write method annotated `@Transactional` | 🔴 | -| SI-06 | Every read-only method annotated `@Transactional(readOnly = true)` | 🟡 | -| SI-07 | No empty `catch` blocks — every catch logs and re-throws or wraps | 🔴 | -| SI-08 | Throws domain exceptions (`ResourceNotFoundException`, `DuplicateResourceException`, `InvalidOperationException`) — not raw `RuntimeException` | 🟡 | -| SI-09 | SLF4J logger declared `private static final Logger log = LoggerFactory.getLogger(...)` | 🟡 | -| SI-10 | Debug log on method entry with non-PII parameters (IDs only) | 🟢 | -| SI-11 | Error log includes exception object as last arg: `log.error("msg id={}", id, e)` | 🟡 | -| SI-12 | No PII in any log statement | 🔴 | -| SI-13 | No Spring MVC imports (`HttpServletRequest`, `HttpServletResponse`) | 🟡 | -| SI-14 | No direct calls to repositories belonging to the wrong DB (iemr vs identity) | 🟡 | -| SI-15 | Sensitive fields encrypted via `CryptoUtil.encrypt()` before save | 🔴 | -| SI-16 | Sensitive fields decrypted via `CryptoUtil.decrypt()` after load | 🔴 | -| SI-17 | No method longer than 50 lines | 🟡 | -| SI-18 | No nesting deeper than 4 levels | 🟢 | - ---- - -## Repository - -> Package: `com.iemr.flw.repo.iemr` or `com.iemr.flw.repo.identity` -> Naming: `<Domain>Repo` or `<Domain>Repository` - -| # | Item | Severity if Failed | -|---|---|---| -| R-01 | Declared as `interface` extending `JpaRepository<Entity, ID>` | 🔴 | -| R-02 | Annotated `@Repository` | 🟡 | -| R-03 | Lives in `repo/iemr/` for `db_iemr` entities; `repo/identity/` for `db_identity` | 🟡 | -| R-04 | Custom queries use `@Query` with JPQL or named native SQL — never string concatenation | 🔴 | -| R-05 | Named parameters used: `@Param("name") Type name` — not positional `?1` | 🟡 | -| R-06 | Soft-delete queries filter `deleted = false` or `Deleted = 0` where applicable | 🟡 | -| R-07 | No business logic in the repository (no `if`, no loops, no service calls) | 🔴 | -| R-08 | Read-only summary queries use projections or DTO constructors, not full entity loads | 🟢 | -| R-09 | `findAll()` never called without a `WHERE` clause on large tables — use paged queries | 🟡 | - ---- - -## DTO - -> Package: `com.iemr.flw.dto.iemr` or `com.iemr.flw.dto.identity` -> Naming: `<Domain>Request` / `<Domain>Response` / `<Domain>DTO` - -| # | Item | Severity if Failed | -|---|---|---| -| D-01 | No `@Entity`, `@Table`, or JPA annotations present | 🔴 | -| D-02 | Request DTOs: all required fields annotated `@NotNull` or `@NotBlank` | 🟡 | -| D-03 | `String` fields have `@Size(max = N)` to prevent oversized input | 🟡 | -| D-04 | ID fields annotated `@Positive` or `@Min(1)` | 🟢 | -| D-05 | Date-of-birth / visit-date fields annotated `@PastOrPresent` | 🟢 | -| D-06 | Separate request DTO and response DTO — not the same class for both directions | 🟢 | -| D-07 | No circular references (DTO → DTO → same DTO) | 🟡 | -| D-08 | Sensitive fields (`aadhaarNumber`, `phoneNumber`) not included in response DTOs that go to lower-privilege clients | 🔴 | -| D-09 | Annotated `@Data` (Lombok) or has explicit getters/setters | 🟡 | - ---- - -## Entity - -> Package: `com.iemr.flw.domain.iemr` or `com.iemr.flw.domain.identity` -> Naming: Plain domain noun (e.g., `AncCare`, `ChildRegister`) - -| # | Item | Severity if Failed | -|---|---|---| -| E-01 | Annotated `@Entity` and `@Table(name = "t_<domain>", schema = "db_iemr")` | 🟡 | -| E-02 | Lives in the correct package for its database | 🟡 | -| E-03 | Primary key: `@Id` + `@GeneratedValue(strategy = GenerationType.IDENTITY)` | 🟡 | -| E-04 | Soft-delete field present: `Boolean deleted = false` or `Deleted` | 🟡 | -| E-05 | Audit fields present: `createdDate`, `createdBy`, `modifiedDate`, `modifiedBy` | 🟢 | -| E-06 | `@OneToMany` collections use `fetch = FetchType.LAZY` | 🟡 | -| E-07 | `@ManyToMany` collections override the default to `fetch = FetchType.LAZY` | 🟡 | -| E-08 | No business logic in entity (no service calls, no derived compute, no `@PrePersist` with logic) | 🔴 | -| E-09 | No Spring annotations inside entity (no `@Autowired`, `@Value`) | 🔴 | -| E-10 | `toString()` does not include sensitive fields (`aadhaarNumber`, `phoneNumber`) | 🔴 | -| E-11 | `equals()` / `hashCode()` based on `id` only — not mutable fields | 🟡 | - ---- - -## Test Class - -> Package: mirrors production package under `src/test/` -> Naming: `<ProductionClass>Test` - -| # | Item | Severity if Failed | -|---|---|---| -| T-01 | `@ExtendWith(MockitoExtension.class)` on the class — not `@RunWith(MockitoJUnitRunner.class)` | 🟡 | -| T-02 | `@DisplayName("...")` on the test class — describes the class under test | 🟢 | -| T-03 | Every dependency is `@Mock`; class under test is `@InjectMocks` | 🟡 | -| T-04 | Every `@Test` method has `@DisplayName("should <behaviour> when <condition>")` | 🟢 | -| T-05 | AAA pattern: Arrange → blank line → Act → blank line → Assert | 🟡 | -| T-06 | Happy-path test exists for every public service method | 🟡 | -| T-07 | Not-found / empty path tested with `assertThrows(ResourceNotFoundException.class, ...)` | 🟡 | -| T-08 | Exception message also asserted inside `assertThrows` — not just the exception type | 🟢 | -| T-09 | Assertions are specific: `assertEquals`, `assertThat`, etc. — not just `assertNotNull` | 🟡 | -| T-10 | `verify(mockRepo).save(any())` used when a save side-effect must be confirmed | 🟢 | -| T-11 | No `Thread.sleep` | 🔴 | -| T-12 | No real PII in test fixtures (no real Aadhaar 12-digit numbers, no real phone numbers) | 🔴 | -| T-13 | Test does not access a real database or Redis — all dependencies are mocked | 🔴 | -| T-14 | No shared mutable state between test methods without `@BeforeEach` reset | 🟡 | -| T-15 | `@BeforeEach` used for common setup — not repeated inside each test | 🟢 | diff --git a/.claude/skills/java-code-writer/SKILL.md b/.claude/skills/java-code-writer/SKILL.md deleted file mode 100644 index 76b6beb0..00000000 --- a/.claude/skills/java-code-writer/SKILL.md +++ /dev/null @@ -1,156 +0,0 @@ -# java-code-writer - -Write production-ready Spring Boot code for the AMRIT / FLW-API platform. - -## Trigger - -Use this skill when asked to: -- Add a new API endpoint or module -- Implement a new service or repository -- Scaffold a full feature (DTO → Entity → Repo → Service → Controller + Test) -- Build an integration with an external system (ERP, LIMS, SMS, notification channel, report format, etc.) - ---- - -## Output Order - -Always produce artifacts in this sequence — never skip a layer: - -1. **Request/Response DTOs** (`dto/`) -2. **JPA Entity** (`domain/iemr/` or `domain/identity/`) -3. **MapStruct Mapper** (`mapper/`) -4. **Repository interface** (`repo/iemr/` or `repo/identity/`) -5. **Service interface** (`service/`) -6. **Service implementation** (`service/impl/`) -7. **Controller** (`controller/`) -8. **Unit test** for the service impl (`src/test/…`) - -Additionally, whenever the feature involves an external integration, multiple output formats, or any behaviour with foreseeable alternative implementations, produce these artifacts **before** the Service layer: - -8a. **Provider/Strategy enum** (`integration/` or relevant package) -8b. **Provider/Strategy interface** (same package) -8c. **Concrete implementation(s)** (`integration/impl/` or `service/impl/`) -8d. **Factory `@Component`** (`integration/` or relevant package) -8e. **Resolver `@Component`** (config-driven selector) - -See `references/factory-patterns.md` for full templates. - -If an artifact already exists, show only the diff (new methods / fields). - ---- - -## Architecture Rules - -- **Controllers** are thin: `@Valid` on request body, one service call, return `ApiResponse`. No `if/else` business logic. -- **Services** own all business logic. Implement a named interface. Never import Spring MVC types. -- **Repositories** are Spring Data JPA interfaces. Custom queries use `@Query` with named parameters — no string concatenation. -- **DTOs** cross the controller ↔ service boundary. Entities never leave the service layer. -- **Mappers** use MapStruct (`@Mapper(componentModel = "spring")`). No manual field copying elsewhere. - -### Factory Pattern Rules (mandatory for integrations and multi-implementation scenarios) - -Apply the Factory pattern whenever a feature has more than one possible implementation or communicates with an external system. **Never couple a service directly to a single concrete integration class.** - -| Scenario | Factory to create | -|---|---| -| External system integration (ERP, LIMS, IoT…) | `IntegrationServiceFactory` | -| Report generation (PDF, Excel, CSV…) | `ReportGeneratorFactory` | -| Notification dispatch (email, SMS, push…) | `NotificationProviderFactory` | -| Validation strategy (rule sets per plant/lab…) | `ValidatorFactory` | -| Storage backend (DB, S3, filesystem…) | `StorageServiceFactory` | - -Rules: -- **Interface declares `getType()`** — always typed with an enum, never a raw string. -- **Factory is `@Component`** injected with `List<ProviderInterface>` — Spring collects all implementations automatically. -- **Active provider is config-driven**: `@Value("${integration.provider}")` in the Resolver, never hard-coded. -- **`InvalidOperationException`** thrown on unknown key — never return `null`. -- **A `MOCK` implementation must always exist** — local dev and unit tests must never depend on a real external system. -- **Adding a new provider = 2 files only**: new enum constant + new `@Service` class. Factory, Resolver, and callers are untouched. - -See `references/factory-patterns.md` for full boilerplate and worked examples. - ---- - -## Injection Pattern - -Always use **constructor injection**. Never use `@Autowired` on fields. - -```java -private final SomeService someService; - -public SomeController(SomeService someService) { - this.someService = someService; -} -``` - -For service implementations with multiple dependencies, generate a `@RequiredArgsConstructor` (Lombok) or explicit constructor. - ---- - -## Logging Standards - -```java -private static final Logger log = LoggerFactory.getLogger(ClassName.class); -``` - -- Log method entry at `DEBUG` with non-PII parameters only. -- Log exceptions at `ERROR` with message and cause: `log.error("Failed to send to ERP for requestId={}", requestId, e)`. -- Never log: names, Aadhaar, phone numbers, addresses, or any field tagged sensitive. -- Never use `System.out.println` or `e.printStackTrace()`. - ---- - -## Security - -- Add `@Valid` to every `@RequestBody` / `@RequestParam` in controller methods. -- Add `@PreAuthorize("hasRole('ROLE_ASHA')")` (or the appropriate AMRIT role) to every controller method. Use role constants from `masterEnum/` — never inline strings. -- Never hardcode passwords, tokens, or keys. Reference `@Value("${property.name}")` or `Environment`. -- Sensitive entity fields must be encrypted/decrypted via `CryptoUtil`. -- Integration credentials (API keys, endpoints, auth tokens for ERP/LIMS/SMS providers) must be read from `@Value` and must reference environment variables in deployed environments. - ---- - -## Validation - -Add Bean Validation annotations to DTO fields: -- `@NotNull`, `@NotBlank`, `@Size`, `@Min`, `@Max`, `@Pattern` as appropriate. -- For ID fields: `@Positive`. -- For date fields: `@PastOrPresent` for date of birth / visit dates. - ---- - -## Exception Handling - -- Throw `ResourceNotFoundException` when a record is not found (maps to 404). -- Throw `DuplicateResourceException` for unique constraint violations (maps to 409). -- Throw `InvalidOperationException` for business rule violations and for unknown factory keys (maps to 422). -- Never swallow exceptions in an empty catch block. -- Wrap checked exceptions from external integrations (`IOException`, `HttpClientErrorException`, etc.) in `InvalidOperationException` with a meaningful message and the original cause. -- See `references/exceptions.md` for exception class definitions. - ---- - -## References - -| File | Contents | -|---|---| -| `references/spring-patterns.md` | Full boilerplate for every layer (DTO → Entity → Repo → Service → Controller → Test) | -| `references/exceptions.md` | Custom exception classes and HTTP mapping table | -| `references/security-patterns.md` | Role constants, `@PreAuthorize` patterns, JWT filter, CryptoUtil usage | -| `references/factory-patterns.md` | **Factory pattern templates** — IntegrationServiceFactory, ReportGeneratorFactory, NotificationProviderFactory, unit tests | - ---- - -## Checklist Before Finalizing Output - -- [ ] Every controller method has `@Valid` and `@PreAuthorize` -- [ ] No entity is returned from a controller -- [ ] Constructor injection used throughout -- [ ] SLF4J logger declared (no System.out) -- [ ] No PII logged -- [ ] Unit test covers happy path, not-found, and exception path -- [ ] `@DisplayName` on test class and every test method -- [ ] No hardcoded secrets -- [ ] **If an integration or multi-impl scenario:** Factory + Resolver + Mock impl present -- [ ] **Provider selection is config-driven** (`application.properties` → env var), not hard-coded -- [ ] **`InvalidOperationException` thrown** (not null returned) when factory key is missing diff --git a/.claude/skills/java-code-writer/references/exceptions.md b/.claude/skills/java-code-writer/references/exceptions.md deleted file mode 100644 index ef4a2bbc..00000000 --- a/.claude/skills/java-code-writer/references/exceptions.md +++ /dev/null @@ -1,158 +0,0 @@ -# Custom Exception Classes — AMRIT / FLW-API - -## Exception Hierarchy - -``` -RuntimeException -├── ResourceNotFoundException → HTTP 404 -├── DuplicateResourceException → HTTP 409 -└── InvalidOperationException → HTTP 422 -``` - ---- - -## ResourceNotFoundException - -Throw when a requested entity does not exist or has been soft-deleted. - -```java -package com.piramal.flwapi.exception; - -public class ResourceNotFoundException extends RuntimeException { - - private final String resourceName; - private final String fieldName; - private final Object fieldValue; - - public ResourceNotFoundException(String resourceName, String fieldName, Object fieldValue) { - super(String.format("%s not found with %s: '%s'", resourceName, fieldName, fieldValue)); - this.resourceName = resourceName; - this.fieldName = fieldName; - this.fieldValue = fieldValue; - } - - public String getResourceName() { return resourceName; } - public String getFieldName() { return fieldName; } - public Object getFieldValue() { return fieldValue; } -} -``` - -**Usage:** -```java -exampleRepository.findByIdAndDeletedFalse(id) - .orElseThrow(() -> new ResourceNotFoundException("AncCare", "id", id)); -``` - ---- - -## DuplicateResourceException - -Throw when a create/update would violate a uniqueness constraint. - -```java -package com.piramal.flwapi.exception; - -public class DuplicateResourceException extends RuntimeException { - - private final String resourceName; - private final String fieldName; - private final Object fieldValue; - - public DuplicateResourceException(String resourceName, String fieldName, Object fieldValue) { - super(String.format("%s already exists with %s: '%s'", resourceName, fieldName, fieldValue)); - this.resourceName = resourceName; - this.fieldName = fieldName; - this.fieldValue = fieldValue; - } - - public String getResourceName() { return resourceName; } - public String getFieldName() { return fieldName; } - public Object getFieldValue() { return fieldValue; } -} -``` - -**Usage:** -```java -if (coupleRepository.existsByBeneficiaryIdAndDeletedFalse(request.getBeneficiaryId())) { - throw new DuplicateResourceException("EligibleCoupleRegister", "beneficiaryId", - request.getBeneficiaryId()); -} -``` - ---- - -## InvalidOperationException - -Throw when a business rule prevents the requested operation. Use for semantic validation that cannot be expressed as Bean Validation annotations. - -```java -package com.piramal.flwapi.exception; - -public class InvalidOperationException extends RuntimeException { - - public InvalidOperationException(String message) { - super(message); - } - - public InvalidOperationException(String message, Throwable cause) { - super(message, cause); - } -} -``` - -**Usage:** -```java -if (deliveryOutcome.getDeliveryDate().isBefore(ancRecord.getLmpDate())) { - throw new InvalidOperationException( - "Delivery date cannot be before last menstrual period date"); -} - -if (tbTreatment.getStatus() == TreatmentStatus.COMPLETED) { - throw new InvalidOperationException( - "Cannot modify a completed TB treatment record"); -} -``` - ---- - -## HTTP Status Mapping - -| Exception | HTTP Status | Spring `@ResponseStatus` | Use case | -|---|---|---|---| -| `ResourceNotFoundException` | 404 Not Found | `HttpStatus.NOT_FOUND` | Entity not found / soft-deleted | -| `DuplicateResourceException` | 409 Conflict | `HttpStatus.CONFLICT` | Unique constraint violation | -| `InvalidOperationException` | 422 Unprocessable Entity | `HttpStatus.UNPROCESSABLE_ENTITY` | Business rule violation | -| `MethodArgumentNotValidException` (Spring) | 400 Bad Request | `HttpStatus.BAD_REQUEST` | Bean Validation failure | -| `AccessDeniedException` (Spring Security) | 403 Forbidden | `HttpStatus.FORBIDDEN` | Missing role/permission | -| `AuthenticationException` (Spring Security) | 401 Unauthorized | `HttpStatus.UNAUTHORIZED` | Invalid/missing JWT | - -All are handled in `GlobalExceptionHandler` — see `references/spring-patterns.md` section 9. - ---- - -## Exception Handling Rules - -1. **Never swallow exceptions.** An empty `catch` block is a bug. -2. **Always log at the right level:** - - `WARN` for 4xx errors (expected bad input, not-found). - - `ERROR` for 5xx errors (unexpected failures). -3. **Never log PII** in exception messages or log statements. -4. **Do not expose stack traces** to API consumers — let `GlobalExceptionHandler` normalize the response. -5. **Wrap checked exceptions** from external libraries (e.g., `IOException` from crypto) in `InvalidOperationException` with a meaningful message. - -```java -// Wrong -try { - return cryptoUtil.decrypt(value); -} catch (Exception e) { - // swallowed! -} - -// Correct -try { - return cryptoUtil.decrypt(value); -} catch (CryptoException e) { - log.error("Failed to decrypt field for record id={}", recordId, e); - throw new InvalidOperationException("Data decryption failed", e); -} -``` diff --git a/.claude/skills/java-code-writer/references/factory-patterns.md b/.claude/skills/java-code-writer/references/factory-patterns.md deleted file mode 100644 index 4605fdde..00000000 --- a/.claude/skills/java-code-writer/references/factory-patterns.md +++ /dev/null @@ -1,364 +0,0 @@ -# Factory Design Patterns — AMRIT / FLW-API - -Use Factory patterns wherever there are **multiple implementations** of a behaviour (integrations, report formats, notification channels, validators, etc.) so future providers can be plugged in without touching existing code. - ---- - -## When to Apply a Factory - -| Scenario | Factory type to use | -|---|---| -| Multiple external integrations (SMS, ERP, LIMS…) | `IntegrationServiceFactory` | -| Multiple report formats (PDF, Excel, CSV…) | `ReportGeneratorFactory` | -| Multiple notification channels (email, push, SMS…) | `NotificationProviderFactory` | -| Multiple validators with shared interface | `ValidatorFactory` | -| Multiple storage backends (DB, S3, local…) | `StorageServiceFactory` | - ---- - -## 1. Provider / Strategy Interface - -Every variant implements a common interface that declares a **type identifier** plus the operation contract. - -```java -package com.piramal.flwapi.integration; - -/** - * Common contract for all external integration providers. - * Implement this interface for every new provider — no other class needs to change. - */ -public interface IntegrationService { - - /** - * Returns the unique provider key used to look this implementation up in - * {@link IntegrationServiceFactory}. Must be UPPER_SNAKE_CASE and match an - * {@code integration.provider} config value. - */ - IntegrationProvider getProvider(); - - /** - * Sends {@code payload} to the external system. - * - * @param payload the data to transmit (never null) - * @return the raw response from the provider - * @throws IntegrationException on any provider-side error - */ - IntegrationResponse send(IntegrationPayload payload); -} -``` - -```java -package com.piramal.flwapi.integration; - -/** Enum of all supported integration providers. Add new values here when onboarding a provider. */ -public enum IntegrationProvider { - SAP_ERP, - ORACLE_ERP, - LIMS_CONNECT, - MOCK // for tests / local dev -} -``` - ---- - -## 2. Concrete Implementations - -Each implementation is a Spring `@Service` — the factory discovers them automatically through injection. - -```java -package com.piramal.flwapi.integration.impl; - -import com.piramal.flwapi.integration.IntegrationPayload; -import com.piramal.flwapi.integration.IntegrationProvider; -import com.piramal.flwapi.integration.IntegrationResponse; -import com.piramal.flwapi.integration.IntegrationService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Service; - -@Service -public class SapErpIntegrationService implements IntegrationService { - - private static final Logger log = LoggerFactory.getLogger(SapErpIntegrationService.class); - - @Override - public IntegrationProvider getProvider() { - return IntegrationProvider.SAP_ERP; - } - - @Override - public IntegrationResponse send(IntegrationPayload payload) { - log.debug("Sending payload to SAP ERP, requestId={}", payload.getRequestId()); - // SAP-specific HTTP/RFC call here - return IntegrationResponse.ok("SAP_ACK_" + payload.getRequestId()); - } -} -``` - -```java -package com.piramal.flwapi.integration.impl; - -import com.piramal.flwapi.integration.IntegrationPayload; -import com.piramal.flwapi.integration.IntegrationProvider; -import com.piramal.flwapi.integration.IntegrationResponse; -import com.piramal.flwapi.integration.IntegrationService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Service; - -/** Stub used in local / test environments — never wired in production. */ -@Service -public class MockIntegrationService implements IntegrationService { - - private static final Logger log = LoggerFactory.getLogger(MockIntegrationService.class); - - @Override - public IntegrationProvider getProvider() { - return IntegrationProvider.MOCK; - } - - @Override - public IntegrationResponse send(IntegrationPayload payload) { - log.debug("Mock integration — swallowing payload for requestId={}", payload.getRequestId()); - return IntegrationResponse.ok("MOCK_OK"); - } -} -``` - ---- - -## 3. The Factory - -The factory is injected with **all** `IntegrationService` beans by Spring and indexes them by provider. - -```java -package com.piramal.flwapi.integration; - -import com.piramal.flwapi.exception.InvalidOperationException; -import org.springframework.stereotype.Component; - -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; - -/** - * Resolves the correct {@link IntegrationService} at runtime based on a configured - * {@link IntegrationProvider} key. - * - * <p>To add a new provider: create a class that implements {@link IntegrationService}, - * annotate it with {@code @Service}, and add its key to {@link IntegrationProvider}. - * No changes needed here or anywhere else. - */ -@Component -public class IntegrationServiceFactory { - - private final Map<IntegrationProvider, IntegrationService> registry; - - /** Spring injects every IntegrationService bean in the application context. */ - public IntegrationServiceFactory(List<IntegrationService> services) { - this.registry = services.stream() - .collect(Collectors.toMap(IntegrationService::getProvider, Function.identity())); - } - - /** - * Returns the integration service for the given provider. - * - * @param provider the target provider - * @return the matching service - * @throws InvalidOperationException if no implementation is registered for the provider - */ - public IntegrationService getService(IntegrationProvider provider) { - IntegrationService service = registry.get(provider); - if (service == null) { - throw new InvalidOperationException( - "No IntegrationService registered for provider: " + provider); - } - return service; - } -} -``` - ---- - -## 4. Driving the Factory from Config - -The active provider is read from `application.properties` — switching providers is a config change, not a code change. - -```properties -# application.properties -integration.provider=SAP_ERP -``` - -```java -package com.piramal.flwapi.integration; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -/** - * Resolves the configured provider once on startup and caches the service. - * Inject this wherever you need to send to the external system. - */ -@Component -public class IntegrationServiceResolver { - - private final IntegrationService activeService; - - public IntegrationServiceResolver( - IntegrationServiceFactory factory, - @Value("${integration.provider}") String providerKey) { - - IntegrationProvider provider = IntegrationProvider.valueOf(providerKey); - this.activeService = factory.getService(provider); - } - - public IntegrationService get() { - return activeService; - } -} -``` - -Usage in a service: -```java -@Service -@RequiredArgsConstructor -public class SampleDispatchService { - - private final IntegrationServiceResolver integrationResolver; - - public void dispatchToErp(Long requestId) { - IntegrationPayload payload = IntegrationPayload.of(requestId); - IntegrationResponse response = integrationResolver.get().send(payload); - log.debug("ERP dispatch completed, reference={}", response.getReference()); - } -} -``` - ---- - -## 5. Report Generator Factory (second example — same pattern) - -```java -// Interface -public interface ReportGenerator { - ReportFormat getFormat(); - byte[] generate(ReportData data); -} - -// Enum -public enum ReportFormat { PDF, EXCEL, CSV } - -// Implementation example -@Service -public class PdfReportGenerator implements ReportGenerator { - @Override public ReportFormat getFormat() { return ReportFormat.PDF; } - @Override public byte[] generate(ReportData data) { /* iText / JasperReports */ return new byte[0]; } -} - -// Factory — identical structure to IntegrationServiceFactory -@Component -public class ReportGeneratorFactory { - private final Map<ReportFormat, ReportGenerator> registry; - public ReportGeneratorFactory(List<ReportGenerator> generators) { - this.registry = generators.stream() - .collect(Collectors.toMap(ReportGenerator::getFormat, Function.identity())); - } - public ReportGenerator getGenerator(ReportFormat format) { - ReportGenerator generator = registry.get(format); - if (generator == null) throw new InvalidOperationException("No generator for format: " + format); - return generator; - } -} -``` - ---- - -## 6. Notification Provider Factory (third example) - -```java -// Interface -public interface NotificationProvider { - NotificationChannel getChannel(); - void send(NotificationPayload payload); -} - -// Enum -public enum NotificationChannel { EMAIL, SMS, PUSH } - -// EMAIL implementation -@Service -public class EmailNotificationProvider implements NotificationProvider { - @Override public NotificationChannel getChannel() { return NotificationChannel.EMAIL; } - @Override public void send(NotificationPayload payload) { /* SMTP / SES */ } -} - -// SMS implementation -@Service -public class SmsNotificationProvider implements NotificationProvider { - @Override public NotificationChannel getChannel() { return NotificationChannel.SMS; } - @Override public void send(NotificationPayload payload) { /* Twilio / AWS SNS */ } -} - -// Factory -@Component -public class NotificationProviderFactory { - private final Map<NotificationChannel, NotificationProvider> registry; - public NotificationProviderFactory(List<NotificationProvider> providers) { - this.registry = providers.stream() - .collect(Collectors.toMap(NotificationProvider::getChannel, Function.identity())); - } - public NotificationProvider getProvider(NotificationChannel channel) { - NotificationProvider provider = registry.get(channel); - if (provider == null) throw new InvalidOperationException("No provider for channel: " + channel); - return provider; - } -} -``` - ---- - -## 7. Unit Testing a Factory - -```java -@DisplayName("IntegrationServiceFactory") -class IntegrationServiceFactoryTest { - - private IntegrationServiceFactory factory; - - @BeforeEach - void setUp() { - factory = new IntegrationServiceFactory( - List.of(new SapErpIntegrationService(), new MockIntegrationService())); - } - - @Test - @DisplayName("returns SAP implementation when provider is SAP_ERP") - void returnsSapService() { - IntegrationService service = factory.getService(IntegrationProvider.SAP_ERP); - assertThat(service).isInstanceOf(SapErpIntegrationService.class); - } - - @Test - @DisplayName("throws InvalidOperationException for unknown provider") - void throwsForUnknownProvider() { - // Force removal to simulate missing implementation - IntegrationServiceFactory emptyFactory = new IntegrationServiceFactory(List.of()); - assertThatThrownBy(() -> emptyFactory.getService(IntegrationProvider.SAP_ERP)) - .isInstanceOf(InvalidOperationException.class) - .hasMessageContaining("SAP_ERP"); - } -} -``` - ---- - -## Factory Pattern Rules - -- **Interface declares `getType()`** — the key method the factory indexes by. Always use an enum, never a string. -- **Factory is `@Component`**, never `@Service` — it orchestrates, it does not implement business logic. -- **Implementations are `@Service`** — Spring auto-discovers them; the factory just collects what's already in the context. -- **Config drives selection** — use `@Value("${…}")` to pick the active provider; never hard-code it. -- **`InvalidOperationException` on missing key** — fail fast with a clear message rather than returning `null`. -- **Mock / stub implementation always exists** — ensures local dev and tests never need a real external system. -- **Adding a new provider = 2 files only**: a new enum constant + a new `@Service` class. Zero changes to factory, resolver, or callers. diff --git a/.claude/skills/java-code-writer/references/security-patterns.md b/.claude/skills/java-code-writer/references/security-patterns.md deleted file mode 100644 index c16735d0..00000000 --- a/.claude/skills/java-code-writer/references/security-patterns.md +++ /dev/null @@ -1,197 +0,0 @@ -# Security Patterns — AMRIT / FLW-API - -## AMRIT Role Constants - -These roles are used in `@PreAuthorize` annotations. Always reference constants — never inline strings. - -```java -package com.piramal.flwapi.masterEnum; - -public final class AmritRoles { - private AmritRoles() {} - - public static final String ASHA = "ROLE_ASHA"; - public static final String ANM = "ROLE_ANM"; - public static final String SUPERVISOR = "ROLE_SUPERVISOR"; - public static final String MO = "ROLE_MO"; // Medical Officer - public static final String ADMIN = "ROLE_ADMIN"; - public static final String VIEWER = "ROLE_VIEWER"; -} -``` - ---- - -## @PreAuthorize Patterns - -```java -// Single role -@PreAuthorize("hasRole('ROLE_ADMIN')") - -// Multiple roles (OR) -@PreAuthorize("hasAnyRole('ROLE_ASHA', 'ROLE_ANM')") - -// Role AND ownership check (use with Principal injection) -@PreAuthorize("hasRole('ROLE_SUPERVISOR') or #userId == authentication.principal.id") - -// Method-level expression using SpEL -@PreAuthorize("hasRole('ROLE_ADMIN') and #request.districtId != null") -``` - -**Recommended per endpoint type:** - -| Endpoint type | Minimum roles | -|---|---| -| Create / Update beneficiary data | `ROLE_ASHA`, `ROLE_ANM` | -| Read beneficiary data | `ROLE_ASHA`, `ROLE_ANM`, `ROLE_SUPERVISOR` | -| Delete / soft-delete | `ROLE_SUPERVISOR`, `ROLE_ADMIN` | -| Reports / dashboards | `ROLE_SUPERVISOR`, `ROLE_MO`, `ROLE_ADMIN` | -| Admin operations | `ROLE_ADMIN` only | - ---- - -## JWT Filter (reference — already implemented) - -The `JwtUserIdValidationFilter` intercepts every request, validates the Bearer token via `JwtUtil`, and populates `SecurityContextHolder`. You do not need to re-implement this. - -```java -// JwtUtil — key methods already available -String extractUsername(String token); -boolean validateToken(String token, UserDetails userDetails); -String extractUserId(String token); -``` - -To extract the current user in a service: - -```java -// Inject via controller and pass as parameter — preferred -public ExampleResponse create(CreateExampleRequest request, String currentUser) { ... } - -// Or read from SecurityContext directly in service (acceptable) -String currentUser = SecurityContextHolder.getContext() - .getAuthentication() - .getName(); -``` - ---- - -## Spring Security Configuration Template - -```java -package com.piramal.flwapi.config; - -import com.piramal.flwapi.utils.JwtUserIdValidationFilter; -import lombok.RequiredArgsConstructor; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; - -@Configuration -@EnableWebSecurity -@EnableMethodSecurity // enables @PreAuthorize -@RequiredArgsConstructor -public class SecurityConfig { - - private final JwtUserIdValidationFilter jwtFilter; - - private static final String[] PUBLIC_PATHS = { - "/actuator/health", - "/swagger-ui/**", - "/v3/api-docs/**", - "/api/v1/auth/login", - "/api/v1/auth/refresh" - }; - - @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - return http - .csrf(csrf -> csrf.disable()) - .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .authorizeHttpRequests(auth -> auth - .requestMatchers(PUBLIC_PATHS).permitAll() - .anyRequest().authenticated() - ) - .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) - .build(); - } - - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(12); - } -} -``` - ---- - -## BCrypt Usage - -```java -// Hashing (e.g., on user creation) -String hashed = passwordEncoder.encode(rawPassword); - -// Verification (e.g., on login) -boolean matches = passwordEncoder.matches(rawPassword, storedHash); -``` - -Never store or log raw passwords. Never use MD5 or SHA-1 for password hashing. - ---- - -## Sensitive Data Encryption via CryptoUtil - -Fields containing PII (Aadhaar, phone, address) must be encrypted at rest using the existing `CryptoUtil`. - -```java -// In service layer — encrypt before save -entity.setAadhaarNumber(cryptoUtil.encrypt(request.getAadhaarNumber())); - -// Decrypt on read -response.setAadhaarNumber(cryptoUtil.decrypt(entity.getAadhaarNumber())); -``` - -Never log the raw (unencrypted) value of any PII field. - ---- - -## Input Validation Security - -Rely on Bean Validation (`@Valid`) at the controller boundary. For free-text fields that may be persisted or displayed: - -```java -// DTO field — prevent XSS via length limit and pattern -@Pattern(regexp = "^[a-zA-Z0-9 .,\\-]*$", message = "Invalid characters in remarks") -@Size(max = 500) -private String remarks; -``` - -For file uploads (not currently used but may be added): -- Always validate MIME type server-side, not just client-supplied `Content-Type`. -- Store uploads outside the web root. - ---- - -## No Hardcoded Secrets - -```java -// WRONG -private static final String SECRET_KEY = "my-super-secret-123"; - -// CORRECT — read from application.properties -@Value("${jwt.secret}") -private String jwtSecret; -``` - -Secrets in `application.properties` must reference environment variables in deployed environments: -```properties -jwt.secret=${JWT_SECRET} -spring.datasource.password=${DB_PASSWORD} -``` - -Never commit `common_local.properties` with real credentials. diff --git a/.claude/skills/java-code-writer/references/spring-patterns.md b/.claude/skills/java-code-writer/references/spring-patterns.md deleted file mode 100644 index 44a44cce..00000000 --- a/.claude/skills/java-code-writer/references/spring-patterns.md +++ /dev/null @@ -1,584 +0,0 @@ -# Spring Boot Code Templates — AMRIT / FLW-API - -## 1. Request DTO - -```java -package com.piramal.flwapi.dto; - -import jakarta.validation.constraints.*; -import lombok.Data; - -@Data -public class CreateExampleRequest { - - @NotNull(message = "beneficiaryId is required") - @Positive - private Long beneficiaryId; - - @NotBlank(message = "visitType is required") - @Size(max = 100) - private String visitType; - - @PastOrPresent(message = "visitDate must not be in the future") - private LocalDate visitDate; -} -``` - -## 2. Response DTO - -```java -package com.piramal.flwapi.dto; - -import lombok.Builder; -import lombok.Data; - -@Data -@Builder -public class ExampleResponse { - private Long id; - private Long beneficiaryId; - private String visitType; - private LocalDate visitDate; - private LocalDateTime createdAt; - private String createdBy; -} -``` - -## 3. JPA Entity (db_iemr) - -```java -package com.piramal.flwapi.domain.iemr; - -import jakarta.persistence.*; -import lombok.Data; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; -import java.time.LocalDate; -import java.time.LocalDateTime; - -@Entity -@Table(name = "t_example") -@Data -public class ExampleRecord { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "beneficiary_id", nullable = false) - private Long beneficiaryId; - - @Column(name = "visit_type", length = 100) - private String visitType; - - @Column(name = "visit_date") - private LocalDate visitDate; - - @Column(name = "deleted", nullable = false) - private Boolean deleted = false; - - @CreationTimestamp - @Column(name = "created_date", updatable = false) - private LocalDateTime createdDate; - - @Column(name = "created_by", length = 50, updatable = false) - private String createdBy; - - @UpdateTimestamp - @Column(name = "last_modified_date") - private LocalDateTime lastModifiedDate; - - @Column(name = "modified_by", length = 50) - private String modifiedBy; -} -``` - -## 4. MapStruct Mapper - -```java -package com.piramal.flwapi.mapper; - -import com.piramal.flwapi.domain.iemr.ExampleRecord; -import com.piramal.flwapi.dto.CreateExampleRequest; -import com.piramal.flwapi.dto.ExampleResponse; -import org.mapstruct.Mapper; -import org.mapstruct.Mapping; - -@Mapper(componentModel = "spring") -public interface ExampleMapper { - - ExampleRecord toEntity(CreateExampleRequest request); - - @Mapping(source = "createdDate", target = "createdAt") - ExampleResponse toResponse(ExampleRecord entity); -} -``` - -## 5. Repository - -```java -package com.piramal.flwapi.repo.iemr; - -import com.piramal.flwapi.domain.iemr.ExampleRecord; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Optional; - -@Repository -public interface ExampleRepository extends JpaRepository<ExampleRecord, Long> { - - List<ExampleRecord> findByBeneficiaryIdAndDeletedFalse(Long beneficiaryId); - - @Query("SELECT e FROM ExampleRecord e WHERE e.beneficiaryId = :benefId AND e.deleted = false ORDER BY e.visitDate DESC") - List<ExampleRecord> findLatestVisitsByBeneficiary(@Param("benefId") Long beneficiaryId); - - Optional<ExampleRecord> findByIdAndDeletedFalse(Long id); -} -``` - -## 6. Service Interface - -```java -package com.piramal.flwapi.service; - -import com.piramal.flwapi.dto.CreateExampleRequest; -import com.piramal.flwapi.dto.ExampleResponse; - -import java.util.List; - -public interface ExampleService { - - ExampleResponse create(CreateExampleRequest request, String createdBy); - - List<ExampleResponse> getAllByBeneficiary(Long beneficiaryId); - - ExampleResponse getById(Long id); - - ExampleResponse update(Long id, CreateExampleRequest request, String modifiedBy); - - void delete(Long id); -} -``` - -## 7. Service Implementation - -```java -package com.piramal.flwapi.service.impl; - -import com.piramal.flwapi.domain.iemr.ExampleRecord; -import com.piramal.flwapi.dto.CreateExampleRequest; -import com.piramal.flwapi.dto.ExampleResponse; -import com.piramal.flwapi.exception.ResourceNotFoundException; -import com.piramal.flwapi.mapper.ExampleMapper; -import com.piramal.flwapi.repo.iemr.ExampleRepository; -import com.piramal.flwapi.service.ExampleService; -import lombok.RequiredArgsConstructor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; -import java.util.stream.Collectors; - -@Service -@RequiredArgsConstructor -public class ExampleServiceImpl implements ExampleService { - - private static final Logger log = LoggerFactory.getLogger(ExampleServiceImpl.class); - - private final ExampleRepository exampleRepository; - private final ExampleMapper exampleMapper; - - @Override - @Transactional - public ExampleResponse create(CreateExampleRequest request, String createdBy) { - log.debug("Creating example record for benefId={}", request.getBeneficiaryId()); - ExampleRecord entity = exampleMapper.toEntity(request); - entity.setCreatedBy(createdBy); - ExampleRecord saved = exampleRepository.save(entity); - log.debug("Created example record id={}", saved.getId()); - return exampleMapper.toResponse(saved); - } - - @Override - @Transactional(readOnly = true) - public List<ExampleResponse> getAllByBeneficiary(Long beneficiaryId) { - log.debug("Fetching example records for benefId={}", beneficiaryId); - return exampleRepository.findByBeneficiaryIdAndDeletedFalse(beneficiaryId) - .stream() - .map(exampleMapper::toResponse) - .collect(Collectors.toList()); - } - - @Override - @Transactional(readOnly = true) - public ExampleResponse getById(Long id) { - ExampleRecord record = exampleRepository.findByIdAndDeletedFalse(id) - .orElseThrow(() -> new ResourceNotFoundException("ExampleRecord", "id", id)); - return exampleMapper.toResponse(record); - } - - @Override - @Transactional - public ExampleResponse update(Long id, CreateExampleRequest request, String modifiedBy) { - log.debug("Updating example record id={}", id); - ExampleRecord record = exampleRepository.findByIdAndDeletedFalse(id) - .orElseThrow(() -> new ResourceNotFoundException("ExampleRecord", "id", id)); - record.setVisitType(request.getVisitType()); - record.setVisitDate(request.getVisitDate()); - record.setModifiedBy(modifiedBy); - return exampleMapper.toResponse(exampleRepository.save(record)); - } - - @Override - @Transactional - public void delete(Long id) { - log.debug("Soft-deleting example record id={}", id); - ExampleRecord record = exampleRepository.findByIdAndDeletedFalse(id) - .orElseThrow(() -> new ResourceNotFoundException("ExampleRecord", "id", id)); - record.setDeleted(true); - exampleRepository.save(record); - } -} -``` - -## 8. Controller - -```java -package com.piramal.flwapi.controller; - -import com.piramal.flwapi.dto.CreateExampleRequest; -import com.piramal.flwapi.dto.ExampleResponse; -import com.piramal.flwapi.service.ExampleService; -import com.piramal.flwapi.utils.ApiResponse; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/api/v1/example") -@RequiredArgsConstructor -public class ExampleController { - - private final ExampleService exampleService; - - @PostMapping - @PreAuthorize("hasAnyRole('ROLE_ASHA', 'ROLE_ANM')") - public ResponseEntity<ApiResponse<ExampleResponse>> create( - @Valid @RequestBody CreateExampleRequest request, - @RequestHeader("Authorization") String authHeader) { - // Extract user from JWT via JwtUtil if needed, or inject Principal - ExampleResponse response = exampleService.create(request, "currentUser"); - return ResponseEntity.status(HttpStatus.CREATED) - .body(ApiResponse.success("Record created", response)); - } - - @GetMapping("/beneficiary/{beneficiaryId}") - @PreAuthorize("hasAnyRole('ROLE_ASHA', 'ROLE_ANM', 'ROLE_SUPERVISOR')") - public ResponseEntity<ApiResponse<List<ExampleResponse>>> getAllByBeneficiary( - @PathVariable @Positive Long beneficiaryId) { - List<ExampleResponse> records = exampleService.getAllByBeneficiary(beneficiaryId); - return ResponseEntity.ok(ApiResponse.success("Records fetched", records)); - } - - @GetMapping("/{id}") - @PreAuthorize("hasAnyRole('ROLE_ASHA', 'ROLE_ANM', 'ROLE_SUPERVISOR')") - public ResponseEntity<ApiResponse<ExampleResponse>> getById(@PathVariable @Positive Long id) { - return ResponseEntity.ok(ApiResponse.success("Record fetched", exampleService.getById(id))); - } - - @PostMapping("/{id}") - @PreAuthorize("hasAnyRole('ROLE_ASHA', 'ROLE_ANM')") - public ResponseEntity<ApiResponse<ExampleResponse>> update( - @PathVariable @Positive Long id, - @Valid @RequestBody CreateExampleRequest request) { - return ResponseEntity.ok(ApiResponse.success("Record updated", exampleService.update(id, request, "currentUser"))); - } - - @PostMapping("/{id}/delete") - @PreAuthorize("hasAnyRole('ROLE_SUPERVISOR', 'ROLE_ADMIN')") - public ResponseEntity<ApiResponse<Void>> delete(@PathVariable @Positive Long id) { - exampleService.delete(id); - return ResponseEntity.ok(ApiResponse.success("Record deleted", null)); - } -} -``` - -## 9. GlobalExceptionHandler - -```java -package com.piramal.flwapi.exception; - -import com.piramal.flwapi.utils.ApiResponse; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.validation.FieldError; -import org.springframework.web.bind.MethodArgumentNotValidException; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RestControllerAdvice; - -import java.util.Map; -import java.util.stream.Collectors; - -@RestControllerAdvice -public class GlobalExceptionHandler { - - private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); - - @ExceptionHandler(ResourceNotFoundException.class) - public ResponseEntity<ApiResponse<Void>> handleNotFound(ResourceNotFoundException ex) { - log.warn("Resource not found: {}", ex.getMessage()); - return ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(ApiResponse.error(ex.getMessage())); - } - - @ExceptionHandler(DuplicateResourceException.class) - public ResponseEntity<ApiResponse<Void>> handleDuplicate(DuplicateResourceException ex) { - log.warn("Duplicate resource: {}", ex.getMessage()); - return ResponseEntity.status(HttpStatus.CONFLICT) - .body(ApiResponse.error(ex.getMessage())); - } - - @ExceptionHandler(InvalidOperationException.class) - public ResponseEntity<ApiResponse<Void>> handleInvalidOp(InvalidOperationException ex) { - log.warn("Invalid operation: {}", ex.getMessage()); - return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY) - .body(ApiResponse.error(ex.getMessage())); - } - - @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity<ApiResponse<Map<String, String>>> handleValidation( - MethodArgumentNotValidException ex) { - Map<String, String> errors = ex.getBindingResult().getFieldErrors().stream() - .collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage, - (a, b) -> a)); - log.warn("Validation failed: {}", errors); - return ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(ApiResponse.error("Validation failed", errors)); - } - - @ExceptionHandler(Exception.class) - public ResponseEntity<ApiResponse<Void>> handleGeneric(Exception ex) { - log.error("Unexpected error", ex); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ApiResponse.error("An unexpected error occurred")); - } -} -``` - -## 10. Pagination Pattern - -```java -// Repository -Page<ExampleRecord> findByBeneficiaryIdAndDeletedFalse(Long beneficiaryId, Pageable pageable); - -// Service -public Page<ExampleResponse> getAllPaged(Long beneficiaryId, int page, int size) { - Pageable pageable = PageRequest.of(page, size, Sort.by("visitDate").descending()); - return exampleRepository.findByBeneficiaryIdAndDeletedFalse(beneficiaryId, pageable) - .map(exampleMapper::toResponse); -} - -// Controller -@GetMapping("/beneficiary/{beneficiaryId}/paged") -@PreAuthorize("hasAnyRole('ROLE_ASHA', 'ROLE_ANM', 'ROLE_SUPERVISOR')") -public ResponseEntity<ApiResponse<Page<ExampleResponse>>> getPaged( - @PathVariable Long beneficiaryId, - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "10") int size) { - return ResponseEntity.ok(ApiResponse.success("Records fetched", - exampleService.getAllPaged(beneficiaryId, page, size))); -} -``` - ---- - -## 11. Factory Pattern — Integration Service (canonical template) - -Use this structure for any feature that talks to an external system or has multiple possible implementations. - -### 11a. Provider Enum - -```java -package com.piramal.flwapi.integration; - -public enum IntegrationProvider { - SAP_ERP, - ORACLE_ERP, - LIMS_CONNECT, - MOCK // always present — used in tests and local dev -} -``` - -### 11b. Provider Interface - -```java -package com.piramal.flwapi.integration; - -public interface IntegrationService { - - /** Key used by the factory to index this implementation. */ - IntegrationProvider getProvider(); - - IntegrationResponse send(IntegrationPayload payload); -} -``` - -### 11c. Concrete Implementation - -```java -package com.piramal.flwapi.integration.impl; - -import com.piramal.flwapi.integration.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Service; - -@Service -public class SapErpIntegrationService implements IntegrationService { - - private static final Logger log = LoggerFactory.getLogger(SapErpIntegrationService.class); - - @Override - public IntegrationProvider getProvider() { return IntegrationProvider.SAP_ERP; } - - @Override - public IntegrationResponse send(IntegrationPayload payload) { - log.debug("Dispatching to SAP ERP, requestId={}", payload.getRequestId()); - // SAP-specific HTTP/RFC logic - return IntegrationResponse.ok("SAP_ACK_" + payload.getRequestId()); - } -} -``` - -### 11d. Mock Implementation (always required) - -```java -package com.piramal.flwapi.integration.impl; - -import com.piramal.flwapi.integration.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Service; - -@Service -public class MockIntegrationService implements IntegrationService { - - private static final Logger log = LoggerFactory.getLogger(MockIntegrationService.class); - - @Override - public IntegrationProvider getProvider() { return IntegrationProvider.MOCK; } - - @Override - public IntegrationResponse send(IntegrationPayload payload) { - log.debug("Mock integration — no-op for requestId={}", payload.getRequestId()); - return IntegrationResponse.ok("MOCK_OK"); - } -} -``` - -### 11e. Factory - -```java -package com.piramal.flwapi.integration; - -import com.piramal.flwapi.exception.InvalidOperationException; -import org.springframework.stereotype.Component; - -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; - -@Component -public class IntegrationServiceFactory { - - private final Map<IntegrationProvider, IntegrationService> registry; - - public IntegrationServiceFactory(List<IntegrationService> services) { - this.registry = services.stream() - .collect(Collectors.toMap(IntegrationService::getProvider, Function.identity())); - } - - public IntegrationService getService(IntegrationProvider provider) { - IntegrationService service = registry.get(provider); - if (service == null) { - throw new InvalidOperationException( - "No IntegrationService registered for provider: " + provider); - } - return service; - } -} -``` - -### 11f. Config-driven Resolver - -```java -package com.piramal.flwapi.integration; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -@Component -public class IntegrationServiceResolver { - - private final IntegrationService activeService; - - public IntegrationServiceResolver( - IntegrationServiceFactory factory, - @Value("${integration.provider}") String providerKey) { - this.activeService = factory.getService(IntegrationProvider.valueOf(providerKey)); - } - - public IntegrationService get() { return activeService; } -} -``` - -`application.properties`: -```properties -integration.provider=${INTEGRATION_PROVIDER:MOCK} -``` - -### 11g. Factory Unit Test - -```java -@DisplayName("IntegrationServiceFactory") -class IntegrationServiceFactoryTest { - - private IntegrationServiceFactory factory; - - @BeforeEach - void setUp() { - factory = new IntegrationServiceFactory( - List.of(new SapErpIntegrationService(), new MockIntegrationService())); - } - - @Test - @DisplayName("returns correct implementation for SAP_ERP provider") - void returnsSapService() { - assertThat(factory.getService(IntegrationProvider.SAP_ERP)) - .isInstanceOf(SapErpIntegrationService.class); - } - - @Test - @DisplayName("throws InvalidOperationException when provider has no registered implementation") - void throwsForUnknownProvider() { - IntegrationServiceFactory empty = new IntegrationServiceFactory(List.of()); - assertThatThrownBy(() -> empty.getService(IntegrationProvider.SAP_ERP)) - .isInstanceOf(InvalidOperationException.class) - .hasMessageContaining("SAP_ERP"); - } -} -```