Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
0c3b09a
Elasticsearch implementation for Beneficiary Search (#123)
vanitha1822 Jan 8, 2026
c7a52a4
fix: age issue while moving to nurse worklist (#130)
vanitha1822 Jan 8, 2026
cce05f3
Optimize the Elasticsearch for better Response Time (#131)
vanitha1822 Jan 30, 2026
2ad699b
fix: remove the bean function to create the index automatically (#133)
vanitha1822 Jan 30, 2026
64e89b8
Fix ES Issue in the Query (#137)
vanitha1822 Feb 19, 2026
39debc2
Nd/vs/fix es (#138)
vanitha1822 Feb 20, 2026
cbe8fe2
fix: enable multi-word fuzzy search requirement (#139)
vanitha1822 Feb 23, 2026
0590a0c
fix: multi-word search (#140)
vanitha1822 Feb 23, 2026
8b862a8
Fix the column mismatch issue in beneficiary search (#142)
vanitha1822 Feb 27, 2026
1cf6c07
add new column in rmnch table for death and child record
SauravBizbRolly Mar 12, 2026
c32deba
add new column in rmnch table for death and child record
SauravBizbRolly Mar 12, 2026
a6818d4
Merge pull request #144 from PSMRI/add/new_column_in_rmnch
SauravBizbRolly Mar 12, 2026
3703ac6
Cherry-pick health and version API enhancements to release-3.6.1 (#145)
DurgaPrasad-54 Mar 12, 2026
08e5d8c
Rebase 3.6.2 (#150)
vanitha1822 Mar 25, 2026
cb67c59
feat(jwt): enhance jwt validation logging and public endpoint check (…
DurgaPrasad-54 Mar 25, 2026
c8e7c94
fix: pom version (#152)
vanitha1822 Mar 26, 2026
b20c801
Add the missing properties for 1097_Preprod (#153)
vanitha1822 Mar 26, 2026
1daa411
docs: add CLAUDE.md for Claude Code guidance
snehar-nd Mar 30, 2026
9e1fa0a
fix: map sexualOrientationID during beneficiary update in 1097
snehar-nd May 11, 2026
8a9b68a
Merge branch 'release-3.6.2' into sn/3.6.2
snehar-nd May 12, 2026
46e4911
Sexual orientation data not reflecting under DB record (#157)
snehar-nd May 12, 2026
2c13c3b
feat: add getBenFamilyDetails API and fix searchFamily queries
snehar-nd May 13, 2026
4340aed
fix: preserve existing occupation and education when incoming values …
snehar-nd May 13, 2026
af82844
fix: replace in-memory queue with SELECT FOR UPDATE SKIP LOCKED for B…
vanitha1822 May 13, 2026
1de7c5b
Merge pull request #158 from PSMRI/sn/3.6.2
snehar-nd May 14, 2026
6f23596
aam-2126 Memeberlist is not displying properly
snehar-nd May 18, 2026
1c2ba64
Merge pull request #160 from PSMRI/sn/3.6.2
snehar-nd May 18, 2026
9d7ded3
fix: aam-2313 phone number leading with zero - removed zero (#161)
snehar-nd May 21, 2026
f9471c8
Allowing numbers with zero is search by phone number. (#162)
snehar-nd May 21, 2026
26f29a8
Merge branch 'main' into release-3.6.2
vanitha1822 May 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# CLAUDE.md - Identity-API

## Project Overview

Identity-API is the beneficiary identity management service for the AMRIT platform. It handles beneficiary creation, search, update, and deduplication across dual database profiles (`db_iemr` main and `db_identity`). It supports RMNCH (Reproductive, Maternal, Newborn, Child, and Adolescent Health) data management, family tagging, Elasticsearch-based beneficiary search, and health ID linkage.

## Tech Stack

- Java 17, Spring Boot 3.2.2, Maven
- Spring Data JPA / Hibernate, MySQL 8.0
- Elasticsearch (Spring Data Elasticsearch for beneficiary search indexing)
- Redis for session management
- Lombok (1.18.36), MapStruct
- SpringDoc OpenAPI (Swagger UI at `/swagger-ui.html`)
- ECS logging (logback-ecs-encoder)
- JaCoCo for test coverage
- Packaged as WAR for Wildfly deployment

## Build & Run

```bash
mvn clean install -DENV_VAR=local # Build
mvn spring-boot:run -DENV_VAR=local # Run locally
mvn -B package --file pom.xml -P <profile> # Package WAR (dev, local, test, ci, uat)
mvn test # Run tests
```

Environment config: `src/main/resources/common_<ENV_VAR>.properties` is copied to `application.properties` at build time.

## Key Packages (`com.iemr.common.identity`)

- **controller/** - REST endpoints:
- `IdentityController` - Core beneficiary CRUD (create, search, update, search by phone/ID/name)
- `IdentityESController` - Elasticsearch-based beneficiary search
- `rmnch/RMNCHMobileAppController` - RMNCH mobile app data sync
- `familyTagging/FamilyTaggingController` - Family tagging and family search
- `elasticsearch/ElasticsearchSyncController` - Elasticsearch sync management
- `health/HealthController` - Health check endpoint
- `version/VersionController` - API version info
- **service/** - Business logic:
- `IdentityService` - Core identity operations
- `rmnch/` - RMNCH beneficiary management
- `familyTagging/` - Family tagging logic
- `elasticsearch/` - Elasticsearch indexing and sync
- `health/` - Health check service
- **domain/** - Core JPA entities for beneficiary data:
- `MBeneficiaryregidmapping` - Beneficiary registration ID mapping
- `MBeneficiaryaddress`, `MBeneficiarycontact`, `MBeneficiaryAccount` - Beneficiary demographics
- `MBeneficiaryfamilymapping` - Family relationships
- `MBeneficiaryconsent` - Consent management
- `VBenAdvanceSearch` - View for advanced search queries
- **data/** - Additional data models:
- `rmnch/` - RMNCH-specific entities (CBAC details, born birth details, household details, NCD/TB/HRP data)
- `elasticsearch/` - Elasticsearch document models and sync job
- `familyTagging/` - Family tagging models
- **dto/** - Data transfer objects for API requests/responses
- **repo/** - Spring Data JPA and Elasticsearch repositories
- **mapper/** - MapStruct mappers for entity-DTO conversion
- **filter/** - Servlet filters
- **security/** - Security configuration
- **utils/** - Utilities (Redis, HTTP, validation, session, gateway, email, exception handling)
- **config/** - Application configuration

## Architecture Notes

- Dual-profile beneficiary storage: main identity in `db_identity`, with mapping to `db_iemr` for AMRIT platform integration
- Elasticsearch integration provides fast full-text beneficiary search with background sync jobs
- RMNCH module handles field-worker mobile app data (CBAC screening, household surveys, birth details)
- Family tagging enables linking beneficiaries into family units with search by family ID
- MapStruct mappers handle complex entity-to-DTO transformations
- Health ID (ABHA) linkage stored per beneficiary for ABDM integration
- Beneficiary deduplication logic via advanced search views
- Artifact ID: `identity-api`, group: `com.iemr.common.identity`, version: 3.6.1
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.iemr.common.identity</groupId>
<artifactId>identity-api</artifactId>
<version>3.6.1</version>
<version>3.6.2</version>

<packaging>war</packaging>

Expand Down
1 change: 0 additions & 1 deletion src/main/environment/1097_ci.properties
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ spring.redis.host=@env.REDIS_HOST@

cors.allowed-origins=@env.CORS_ALLOWED_ORIGINS@


# Elasticsearch Configuration
elasticsearch.host=@env.ELASTICSEARCH_HOST@
elasticsearch.port=@env.ELASTICSEARCH_PORT@
Expand Down
1 change: 0 additions & 1 deletion src/main/environment/1097_example.properties
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,3 @@ elasticsearch.index.beneficiary=beneficiary_index
# Enable/Disable ES (for gradual rollout)
elasticsearch.enabled=true


Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,20 @@ public String untagFamily(@RequestBody String comingReq) {
return response.toString();
}

@Operation(summary = "Get family tagging details by beneficiary ID")
@PostMapping(value = { "/getBenFamilyDetails" }, consumes = "application/json", produces = "application/json")
public String getFamilyDetailsByBeneficiaryId(@RequestBody String comingReq) {
OutputResponse response = new OutputResponse();
try {
String s = familyTagService.getFamilyDetailsByBeneficiaryId(comingReq);
response.setResponse(s);
} catch (Exception e) {
logger.error("Error in fetching family details by beneficiary ID : " + e);
response.setError(5000, "Error in fetching family details by beneficiary ID : " + e.getLocalizedMessage());
}
return response.toString();
}

@Operation(summary = "Edit beneficiary family details")
@PostMapping(value = { "/editFamilyTagging" }, consumes = "application/json", produces = "application/json")
public String editFamilyDetails(@RequestBody String comingReq) {
Expand Down
10 changes: 8 additions & 2 deletions src/main/java/com/iemr/common/identity/repo/BenContactRepo.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,12 @@
List<MBeneficiarycontact> findByPreferredSMSPhoneNumOrderByBenContactsIDAsc(String smsPhoneNum);


@Query("select c from MBeneficiarycontact c where c.preferredPhoneNum = :phoneNum ")
List<MBeneficiarycontact> findByAnyPhoneNum(@Param("phoneNum") String phoneNum);
// @Query("select c from MBeneficiarycontact c where c.preferredPhoneNum = :phoneNum ")
// List<MBeneficiarycontact> findByAnyPhoneNum(@Param("phoneNum") String phoneNum);

Check warning on line 53 in src/main/java/com/iemr/common/identity/repo/BenContactRepo.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This block of commented-out lines of code should be removed.

See more on https://sonarcloud.io/project/issues?id=PSMRI_Identity-API&issues=AZ5OK5rpFcSfan2ep8Wa&open=AZ5OK5rpFcSfan2ep8Wa&pullRequest=163

@Query("select c from MBeneficiarycontact c where c.preferredPhoneNum IN :variants")
List<MBeneficiarycontact> findByAnyPhoneNum(@Param("variants") List<String> variants);



@Query("select c from MBeneficiarycontact c where c.preferredPhoneNum = :phoneNum or c.phoneNum1 = :phoneNum "
Expand All @@ -70,4 +74,6 @@
MBeneficiarycontact getWithVanSerialNoVanID(@Param("vanSerialNo") BigInteger vanSerialNo,
@Param("vanID") Integer vanID);



}
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,13 @@ MBeneficiaryregidmapping getWithVanSerialNoVanID(@Param("vanSerialNo") BigIntege

List<MBeneficiaryregidmapping> findTop10000ByProvisionedAndReserved(Boolean isProvisioned,Boolean isReserved);

/**
* Atomically selects and locks the next available registration ID row.
* SKIP LOCKED ensures concurrent servers each get a distinct row without blocking each other,
* eliminating duplicate BenRegId assignments when multiple app instances share the same database.
*/
@Transactional
@Query(value = "SELECT * FROM m_beneficiaryregidmapping WHERE Provisioned = false AND Reserved = false ORDER BY BenRegId ASC LIMIT 1 FOR UPDATE SKIP LOCKED", nativeQuery = true)
MBeneficiaryregidmapping findAndLockNextAvailable();

}
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,10 @@ public interface FamilyTagRepo extends CrudRepository<BenFamilyMapping,Long> {
public int untagFamily(@Param("benFamilyTagId") List<Long> benFamilyTagId,@Param("modifiedBy") String modifiedBy);


@Query("SELECT obj FROM BenFamilyMapping obj WHERE obj.familyName =:familyName AND obj.villageId =:villageId AND (obj.noOfmembers is not null "
+ " AND obj.noOfmembers >0)")
@Query("SELECT obj FROM BenFamilyMapping obj WHERE obj.familyName LIKE CONCAT(:familyName, '%') AND (:villageId IS NULL OR obj.villageId =:villageId) AND (obj.deleted IS NULL OR obj.deleted = false)")
List<BenFamilyMapping> searchFamily(@Param("familyName") String familyName,@Param("villageId") Integer villageId);

@Query("SELECT obj FROM BenFamilyMapping obj WHERE obj.familyName =:familyName AND obj.villageId =:villageId AND obj.familyId =:familyId AND (obj.noOfmembers is not null "
+ " AND obj.noOfmembers >0)")

@Query("SELECT obj FROM BenFamilyMapping obj WHERE obj.familyName LIKE CONCAT(:familyName, '%') AND (:villageId IS NULL OR obj.villageId =:villageId) AND obj.familyId =:familyId AND (obj.deleted IS NULL OR obj.deleted = false)")
List<BenFamilyMapping> searchFamilyWithFamilyId(@Param("familyName") String familyName,@Param("villageId") Integer villageId,@Param("familyId") String familyId);

@Query("SELECT obj FROM BenFamilyMapping obj WHERE obj.familyId =:familyId")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* 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;

import java.sql.Timestamp;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import com.iemr.common.identity.domain.MBeneficiaryregidmapping;
import com.iemr.common.identity.repo.BenRegIdMappingRepo;

/**
* Handles atomic beneficiary registration ID claiming.
*
* Uses SELECT ... FOR UPDATE SKIP LOCKED so that each application instance
* obtains a distinct, exclusive row. Multiple servers sharing the same database
* will never receive the same BenRegId, preventing
* SQLIntegrityConstraintViolationException duplicate-key errors that occurred
* with the previous in-memory ArrayDeque queue approach.
*
* REQUIRES_NEW propagation ensures the SELECT + UPDATE happens in its own
* short-lived transaction, releasing the row lock immediately after the ID is
* marked reserved β€” keeping lock contention to a minimum.
*/
@Service
public class BenRegIdClaimService {

private static final Logger logger = LoggerFactory.getLogger(BenRegIdClaimService.class);

@Autowired
private BenRegIdMappingRepo regIdRepo;

/**
* Atomically claims the next available registration ID.
*
* <ol>
* <li>Opens a brand-new transaction (REQUIRES_NEW).</li>
* <li>Executes SELECT … FOR UPDATE SKIP LOCKED to lock exactly one row.
* Concurrent callers on other servers/threads skip the locked row and
* get the next one β€” no two callers ever see the same row.</li>
* <li>Marks the row {@code reserved = true} and flushes it within the same
* transaction so the change is visible to other connections the moment
* this method returns.</li>
* </ol>
*
* @return the reserved {@link MBeneficiaryregidmapping} with {@code reserved=true}
* @throws IllegalStateException if the ID pool is exhausted
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public MBeneficiaryregidmapping claimNextAvailableRegId() {
MBeneficiaryregidmapping regMap = regIdRepo.findAndLockNextAvailable();
if (regMap == null) {
throw new IllegalStateException(
"No available registration IDs in the pool. "
+ "Please contact the system administrator to import more IDs.");
}
if (regMap.getCreatedDate() == null) {
regMap.setCreatedDate(new Timestamp(System.currentTimeMillis()));
}
regMap.setReserved(true);
regMap = regIdRepo.save(regMap);
logger.info("BenRegIdClaimService: claimed BenRegId={}", regMap.getBenRegId());
return regMap;
}
}
60 changes: 33 additions & 27 deletions src/main/java/com/iemr/common/identity/service/IdentityService.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
import java.math.BigInteger;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
Expand Down Expand Up @@ -154,6 +154,8 @@
@Autowired
BenRegIdMappingRepo regIdRepo;
@Autowired
private BenRegIdClaimService benRegIdClaimService;
@Autowired
BenServiceMappingRepo serviceMapRepo;
@Autowired
MBeneficiaryAccountRepo accountRepo;
Expand Down Expand Up @@ -555,7 +557,15 @@
List<BeneficiariesDTO> list = new ArrayList<>();

try {
List<MBeneficiarycontact> benContact = contactRepo.findByAnyPhoneNum(phoneNum);
// List<MBeneficiarycontact> benContact = contactRepo.findByAnyPhoneNum(phoneNum);

Check warning on line 560 in src/main/java/com/iemr/common/identity/service/IdentityService.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This block of commented-out lines of code should be removed.

See more on https://sonarcloud.io/project/issues?id=PSMRI_Identity-API&issues=AZ5OK5vCFcSfan2ep8Wb&open=AZ5OK5vCFcSfan2ep8Wb&pullRequest=163

String clean = phoneNum.trim();
if (clean.startsWith("+91")) clean = clean.substring(3);
else if (clean.startsWith("91") && clean.length() == 12) clean = clean.substring(2);
else if (clean.startsWith("0") && clean.length() == 11) clean = clean.substring(1);

List<String> variants = Arrays.asList(clean, "0" + clean, "91" + clean, "+91" + clean);
List<MBeneficiarycontact> benContact = contactRepo.findByAnyPhoneNum(variants);

logger.info(benContact.size() + " contacts found for phone number " + phoneNum);

Expand Down Expand Up @@ -1038,6 +1048,18 @@
if (benDetails.getOther() != null) {
mbDetl.setOther(benDetails.getOther());
}
if (mbDetl.getOccupationId() == null && benDetails.getOccupationId() != null) {
mbDetl.setOccupationId(benDetails.getOccupationId());
}
if (mbDetl.getOccupation() == null && benDetails.getOccupation() != null) {
mbDetl.setOccupation(benDetails.getOccupation());
}
if (mbDetl.getEducationId() == null && benDetails.getEducationId() != null) {
mbDetl.setEducationId(benDetails.getEducationId());
}
if (mbDetl.getEducation() == null && benDetails.getEducation() != null) {
mbDetl.setEducation(benDetails.getEducation());
}

// Extract and set extra fields
// String identityJson = new Gson().toJson(json);
Expand Down Expand Up @@ -1337,6 +1359,8 @@
if (dto.getOtherFields() != null) {
beneficiarydetail.setOtherFields(dto.getOtherFields());
}
beneficiarydetail.setSexualOrientationID(dto.getSexualOrientationID());
beneficiarydetail.setSexualOrientationType(dto.getSexualOrientationType());

return beneficiarydetail;
}
Expand All @@ -1345,34 +1369,13 @@
* @param identity
* @return
*/
ArrayDeque<MBeneficiaryregidmapping> queue = new ArrayDeque<>();

public BeneficiaryCreateResp createIdentity(IdentityDTO identity) {
logger.info("IdentityService.createIdentity - start");

List<MBeneficiaryregidmapping> list = null;
MBeneficiaryregidmapping regMap = null;
synchronized (queue) {
if (queue.isEmpty()) {
logger.info("fetching 10000 rows");
list = regIdRepo.findTop10000ByProvisionedAndReserved(false, false);
logger.info("Adding SynchronousQueue start-- ");
for (MBeneficiaryregidmapping map : list) {
queue.add(map);
}
logger.info("Adding SynchronousQueue end-- ");
}
regMap = queue.removeFirst();
}
regMap.setReserved(true);
if (regMap.getCreatedDate() == null) {
SimpleDateFormat sdf = new SimpleDateFormat(CREATED_DATE_FORMAT);
String dateToStoreInDataBase = sdf.format(new Date());
Timestamp ts = Timestamp.valueOf(dateToStoreInDataBase);
regMap.setCreatedDate(ts);
}

regIdRepo.save(regMap);
// 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.
MBeneficiaryregidmapping regMap = benRegIdClaimService.claimNextAvailableRegId();

regMap.setProvisioned(true);

Expand Down Expand Up @@ -1665,6 +1668,9 @@
} else if (cleaned.startsWith("91") && cleaned.length() == 12) {
// Handle case where + is already removed but 91 remains
cleaned = cleaned.substring(2);
} else if (cleaned.startsWith("0") && cleaned.length() == 11) {
// Handle case where number starts with 0 and is 11 digits long
cleaned = cleaned.substring(1);
}

return cleaned.trim();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,6 @@ public interface FamilyTagService {
public String searchFamily(String request) throws IEMRException;

public String editFamilyDetails(String request) throws IEMRException;

public String getFamilyDetailsByBeneficiaryId(String request) throws IEMRException;
}
Loading
Loading