From 1c4baa2776e054506c84824cff3b6c00aa361849 Mon Sep 17 00:00:00 2001 From: Sehjot Singh Pannu Date: Fri, 9 Oct 2026 15:46:14 +0530 Subject: [PATCH 1/3] Removing gpsLatitude and gpsLongitude, and setting values into latitude and longitude --- .../flw/domain/identity/RMNCHBeneficiaryDetailsRmnch.java | 6 ------ .../com/iemr/flw/service/impl/BeneficiaryServiceImpl.java | 6 ------ 2 files changed, 12 deletions(-) diff --git a/src/main/java/com/iemr/flw/domain/identity/RMNCHBeneficiaryDetailsRmnch.java b/src/main/java/com/iemr/flw/domain/identity/RMNCHBeneficiaryDetailsRmnch.java index fa5f435fc..8354670ee 100644 --- a/src/main/java/com/iemr/flw/domain/identity/RMNCHBeneficiaryDetailsRmnch.java +++ b/src/main/java/com/iemr/flw/domain/identity/RMNCHBeneficiaryDetailsRmnch.java @@ -102,12 +102,6 @@ public class RMNCHBeneficiaryDetailsRmnch { @Column(name = "longitude") private BigDecimal longitude; - @Column(name = "gpsLatitude") - private Double gpsLatitude; - - @Column(name = "gpsLongitude") - private Double gpsLongitude; - @Expose @Column(name = "digipin") private String digipin; diff --git a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java index 396c79a2d..596517b78 100644 --- a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java @@ -363,12 +363,6 @@ private String getMappingsForAddressIDs(List addressLi if (benAddressOBJ.getPermPinCode() != null) benDetailsRMNCH_OBJ.setPinCode(benAddressOBJ.getPermPinCode()); - // Map GPS double fields to the exposed latitude/longitude BigDecimal fields for response - if (benDetailsRMNCH_OBJ.getGpsLatitude() != null) - benDetailsRMNCH_OBJ.setLatitude(BigDecimal.valueOf(benDetailsRMNCH_OBJ.getGpsLatitude())); - if (benDetailsRMNCH_OBJ.getGpsLongitude() != null) - benDetailsRMNCH_OBJ.setLongitude(BigDecimal.valueOf(benDetailsRMNCH_OBJ.getGpsLongitude())); - // ----------------------------------------------------------------------------- // related benids From 8c2b33549d9b55ffe3973e05b5bc7bf263325851 Mon Sep 17 00:00:00 2001 From: Sehjot Singh Pannu Date: Fri, 9 Oct 2026 16:28:15 +0530 Subject: [PATCH 2/3] Adding locking for multiple order creation --- .../impl/DiagnosticOrderServiceImpl.java | 88 +++++++++++++++---- 1 file changed, 71 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/iemr/flw/service/impl/DiagnosticOrderServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/DiagnosticOrderServiceImpl.java index 4d62aac50..d5087bd41 100644 --- a/src/main/java/com/iemr/flw/service/impl/DiagnosticOrderServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/DiagnosticOrderServiceImpl.java @@ -41,6 +41,9 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; +import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; @Service public class DiagnosticOrderServiceImpl implements DiagnosticOrderService { @@ -116,7 +119,33 @@ public DiagnosticOrder createAndPushOrderAsSystem(DiagnosticOrderRequestDto requ return createAndPushOrder(request, "SYSTEM"); } + // One lock per beneficiary, held across visit lookup → dedup check → insert → vendor push. Without it, + // two simultaneous pushes for the same beneficiary could both pass the dedup check (or both create a + // visit for today) and send the same patient to the vendor twice — shown as duplicate names on the + // TrueNat machine. Keyed by beneficiary, not orderType, because the visit is shared across order types. + // In-memory, so it only serialises within this JVM: correct while orders are created only by the + // single van server that can reach the vendor. Reentrant, so nested calls (e.g. push → closeOrder) + // don't deadlock. + private final ConcurrentHashMap beneficiaryOrderLocks = new ConcurrentHashMap<>(); + + private T withBeneficiaryLock(Long beneficiaryId, Callable action) throws Exception { + if (beneficiaryId == null) { + throw new IllegalArgumentException("beneficiaryId is required"); + } + ReentrantLock lock = beneficiaryOrderLocks.computeIfAbsent(beneficiaryId, id -> new ReentrantLock()); + lock.lock(); + try { + return action.call(); + } finally { + lock.unlock(); + } + } + private DiagnosticOrder createAndPushOrder(DiagnosticOrderRequestDto request, String createdBy) throws Exception { + return withBeneficiaryLock(request.getBeneficiaryId(), () -> createAndPushOrderLocked(request, createdBy)); + } + + private DiagnosticOrder createAndPushOrderLocked(DiagnosticOrderRequestDto request, String createdBy) throws Exception { Long beneficiaryId = request.getBeneficiaryId(); DiagnosticOrderType orderType = DiagnosticOrderType.fromCode(request.getOrderType()); String orderEvent = request.getOrderEvent(); @@ -169,7 +198,7 @@ private DiagnosticOrder createAndPushOrder(DiagnosticOrderRequestDto request, St && !visitCode.equals(latestForType.get().getVisitCode())) { DiagnosticOrder blocker = latestForType.get(); logger.info("Duplicate order push suppressed for beneficiaryId={}, orderType={}: unresolved order id={} " - + "(visitCode={}, status={}) already exists — returning it instead of pushing a new order for visitCode={}", + + "(visitCode={}, status={}) already exists — returning it instead of pushing a new order for visitCode={}", beneficiaryId, orderType, blocker.getId(), blocker.getVisitCode(), blocker.getStatus(), visitCode); return blocker; } @@ -252,8 +281,8 @@ private DiagnosticOrder pushToProvider(DiagnosticOrder order, String providerCod // with reasonToClose) — resolves the same visit/provider/externalOrderId a normal push would, then // closes the order via saveRefusedOrder. Identical outcome regardless of which endpoint triggered it. private DiagnosticOrder closeOrder(Long beneficiaryId, DiagnosticOrderType orderType, String orderEvent, - String patientFirstName, String patientLastName, String patientDateOfBirth, String patientSex, - String reasonToClose, String actingUserId) throws Exception { + String patientFirstName, String patientLastName, String patientDateOfBirth, String patientSex, + String reasonToClose, String actingUserId) throws Exception { Integer vanID = campConfigService.getVanID(); Integer parkingPlaceID = campConfigService.getParkingPlaceID(); @@ -275,14 +304,16 @@ private DiagnosticOrder closeOrder(Long beneficiaryId, DiagnosticOrderType order // from the beneficiary's own most recent order for this orderType (whatever its status), since that // information already exists there. No prior order at all means there's nothing to source it from. private DiagnosticOrder closeOrderManually(Long beneficiaryId, DiagnosticOrderType orderType, - String reasonToClose, String actingUserId) throws Exception { - DiagnosticOrder source = diagnosticOrderRepo - .findFirstByBeneficiaryIdAndOrderTypeAndDeletedFalseOrderByCreatedDateDesc(beneficiaryId, orderType.name()) - .orElseThrow(() -> new Exception("No diagnostic order found for beneficiaryId=" + beneficiaryId - + ", orderType=" + orderType.name() + " — cannot close a record that was never created")); - return closeOrder(beneficiaryId, orderType, source.getOrderEvent(), source.getPatientFirstName(), - source.getPatientLastName(), source.getPatientDateOfBirth(), source.getPatientSex(), reasonToClose, - actingUserId); + String reasonToClose, String actingUserId) throws Exception { + return withBeneficiaryLock(beneficiaryId, () -> { + DiagnosticOrder source = diagnosticOrderRepo + .findFirstByBeneficiaryIdAndOrderTypeAndDeletedFalseOrderByCreatedDateDesc(beneficiaryId, orderType.name()) + .orElseThrow(() -> new Exception("No diagnostic order found for beneficiaryId=" + beneficiaryId + + ", orderType=" + orderType.name() + " — cannot close a record that was never created")); + return closeOrder(beneficiaryId, orderType, source.getOrderEvent(), source.getPatientFirstName(), + source.getPatientLastName(), source.getPatientDateOfBirth(), source.getPatientSex(), reasonToClose, + actingUserId); + }); } // Refusals are keyed to the latest order for this beneficiary+orderType (not the exact visitCode @@ -291,9 +322,9 @@ private DiagnosticOrder closeOrderManually(Long beneficiaryId, DiagnosticOrderTy // and a new CLOSED row is created instead, so its history (e.g. a FAILED row's errorMessage) survives. // Refused orders are saved as-is and never pushed to the vendor. private DiagnosticOrder saveRefusedOrder(Long beneficiaryId, Long visitCode, DiagnosticOrderType orderType, - String orderEvent, String providerCode, String externalOrderId, String patientFirstName, - String patientLastName, String patientDateOfBirth, String patientSex, String reasonToClose, - String actingUserId) { + String orderEvent, String providerCode, String externalOrderId, String patientFirstName, + String patientLastName, String patientDateOfBirth, String patientSex, String reasonToClose, + String actingUserId) { Optional latest = diagnosticOrderRepo .findFirstByBeneficiaryIdAndOrderTypeAndDeletedFalseOrderByCreatedDateDesc(beneficiaryId, orderType.name()); if (latest.isPresent() && NON_REUSABLE_ON_CLOSE_STATUSES.contains(latest.get().getStatus())) { @@ -371,7 +402,7 @@ public DiagnosticOrderResultDto processResult(DiagnosticOrder order, DiagnosticP } private DiagnosticOrderResultDto processResult(DiagnosticOrder order, DiagnosticPollResult pollResult, - boolean writeBackWhenClosed, String actingUser) throws Exception { + boolean writeBackWhenClosed, String actingUser) throws Exception { Optional existingResult = diagnosticResultRepo.findByExternalOrderIdAndDeletedFalse(order.getExternalOrderId()); DiagnosticResult result = existingResult.orElseGet(DiagnosticResult::new); result.setExternalOrderId(order.getExternalOrderId()); @@ -620,7 +651,7 @@ private DiagnosticOrderResultDto toResultDto(DiagnosticOrder order) { @Override public DiagnosticOrderStatusSummaryDto getOrderStatusSummary(String orderType, Integer villageId, - Integer providerServiceMapId) { + Integer providerServiceMapId) { DiagnosticOrderType type = DiagnosticOrderType.fromCode(orderType); List awaitingProviderResult = diagnosticOrderRepo .findBeneficiaryIdsAwaitingProviderResult(type.name(), villageId, providerServiceMapId); @@ -708,6 +739,29 @@ private static boolean isInvalidResult(String orderTypeCode, String resultSummar // order's beneficiary, visit, vendor and patient details, with only a fresh externalOrderId, // pushed to the vendor straight away. Never throws — a failure here must not undo the close. private void pushRetestOrder(DiagnosticOrder closed) { + try { + withBeneficiaryLock(closed.getBeneficiaryId(), () -> { + pushRetestOrderLocked(closed); + return null; + }); + } catch (Exception e) { + logger.error("Failed to create retest order after invalid result, closedOrderId={}: {}", + closed.getId(), e.getMessage()); + } + } + + private void pushRetestOrderLocked(DiagnosticOrder closed) { + // A user push may have created a fresh order for this beneficiary+orderType after this one was + // closed — the retest would then put the same patient on the vendor twice, so skip it. + Optional latest = diagnosticOrderRepo + .findFirstByBeneficiaryIdAndOrderTypeAndDeletedFalseOrderByCreatedDateDesc( + closed.getBeneficiaryId(), closed.getOrderType()); + if (latest.isPresent() && !latest.get().getId().equals(closed.getId()) + && BLOCKING_STATUSES.contains(latest.get().getStatus())) { + logger.info("Retest skipped for closedOrderId={}: newer order id={} (status={}) already exists", + closed.getId(), latest.get().getId(), latest.get().getStatus()); + return; + } try { DiagnosticOrder retest = new DiagnosticOrder(); retest.setVanID(closed.getVanID()); @@ -730,7 +784,7 @@ private void pushRetestOrder(DiagnosticOrder closed) { retest = diagnosticOrderRepo.save(retest); retest = pushToProvider(retest, closed.getProviderCode()); logger.info("Retest order created after invalid result: closedOrderId={}, retestOrderId={}, " - + "externalOrderId={}, status={}", closed.getId(), retest.getId(), retest.getExternalOrderId(), + + "externalOrderId={}, status={}", closed.getId(), retest.getId(), retest.getExternalOrderId(), retest.getStatus()); } catch (Exception e) { logger.error("Failed to create retest order after invalid result, closedOrderId={}: {}", From 688aafa73e7838d858b1d50c06889412f1809f90 Mon Sep 17 00:00:00 2001 From: Sehjot Singh Pannu Date: Fri, 9 Oct 2026 18:16:59 +0530 Subject: [PATCH 3/3] Adding locking for multiple order creation --- .../service/impl/DiagnosticOrderServiceImpl.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/iemr/flw/service/impl/DiagnosticOrderServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/DiagnosticOrderServiceImpl.java index d5087bd41..e206afbcb 100644 --- a/src/main/java/com/iemr/flw/service/impl/DiagnosticOrderServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/DiagnosticOrderServiceImpl.java @@ -30,6 +30,7 @@ import com.iemr.flw.service.DiagnosticOrderService; import com.iemr.flw.service.TBStopVisitService; import com.iemr.flw.utils.JwtUtil; +import com.google.common.util.concurrent.Striped; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -42,8 +43,7 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.Callable; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.locks.Lock; @Service public class DiagnosticOrderServiceImpl implements DiagnosticOrderService { @@ -124,15 +124,18 @@ public DiagnosticOrder createAndPushOrderAsSystem(DiagnosticOrderRequestDto requ // visit for today) and send the same patient to the vendor twice — shown as duplicate names on the // TrueNat machine. Keyed by beneficiary, not orderType, because the visit is shared across order types. // In-memory, so it only serialises within this JVM: correct while orders are created only by the - // single van server that can reach the vendor. Reentrant, so nested calls (e.g. push → closeOrder) - // don't deadlock. - private final ConcurrentHashMap beneficiaryOrderLocks = new ConcurrentHashMap<>(); + // single van server that can reach the vendor. Striped keeps the lock registry a fixed size for the + // JVM's lifetime: the same beneficiary always maps to the same stripe, while unrelated beneficiaries + // that happen to share a stripe merely wait on each other briefly. Stripes are reentrant, and no path + // ever holds two beneficiaries' locks at once, so sharing a stripe can't deadlock. + private static final int BENEFICIARY_LOCK_STRIPES = 1024; + private final Striped beneficiaryOrderLocks = Striped.lock(BENEFICIARY_LOCK_STRIPES); private T withBeneficiaryLock(Long beneficiaryId, Callable action) throws Exception { if (beneficiaryId == null) { throw new IllegalArgumentException("beneficiaryId is required"); } - ReentrantLock lock = beneficiaryOrderLocks.computeIfAbsent(beneficiaryId, id -> new ReentrantLock()); + Lock lock = beneficiaryOrderLocks.get(beneficiaryId); lock.lock(); try { return action.call();