From ec3ec6eaac1f8e6c3e5da728c3e8d91c5da6cf76 Mon Sep 17 00:00:00 2001 From: Pankaj Kumar Date: Mon, 2 Nov 2026 18:08:25 +0530 Subject: [PATCH 1/7] changes for the benificiary phase sync --- .../nikshay/NikshaySyncStatusController.java | 184 +++++++++ .../mmu/data/nikshay/BeneficiaryNameRow.java | 11 + .../mmu/data/nikshay/Beneficiarydetail.java | 231 +++++++++++ .../NikshayBeneficiaryPhaseStatus.java | 75 ++++ .../nikshay/NikshayBeneficiarySyncStatus.java | 72 ++++ .../nikshay/NikshayMissingBeneficiaryDTO.java | 27 ++ .../nikshay/NikshayMissingBeneficiaryRow.java | 15 + .../nikshay/NikshaySyncStatusUpdateDTO.java | 13 + .../mmu/repo/nikshay/BeneficiaryRepo.java | 20 + .../NikshayBeneficiaryPhaseStatusRepo.java | 23 ++ .../NikshayBeneficiarySyncStatusRepo.java | 87 +++++ .../nikshay/NikshaySyncStatusService.java | 18 + .../impl/NikshaySyncStatusServiceImpl.java | 360 ++++++++++++++++++ .../com/iemr/mmu/utils/NikshayConstants.java | 16 + 14 files changed, 1152 insertions(+) create mode 100644 src/main/java/com/iemr/mmu/controller/nikshay/NikshaySyncStatusController.java create mode 100644 src/main/java/com/iemr/mmu/data/nikshay/BeneficiaryNameRow.java create mode 100644 src/main/java/com/iemr/mmu/data/nikshay/Beneficiarydetail.java create mode 100644 src/main/java/com/iemr/mmu/data/nikshay/NikshayBeneficiaryPhaseStatus.java create mode 100644 src/main/java/com/iemr/mmu/data/nikshay/NikshayBeneficiarySyncStatus.java create mode 100644 src/main/java/com/iemr/mmu/data/nikshay/NikshayMissingBeneficiaryDTO.java create mode 100644 src/main/java/com/iemr/mmu/data/nikshay/NikshayMissingBeneficiaryRow.java create mode 100644 src/main/java/com/iemr/mmu/data/nikshay/NikshaySyncStatusUpdateDTO.java create mode 100644 src/main/java/com/iemr/mmu/repo/nikshay/BeneficiaryRepo.java create mode 100644 src/main/java/com/iemr/mmu/repo/nikshay/NikshayBeneficiaryPhaseStatusRepo.java create mode 100644 src/main/java/com/iemr/mmu/repo/nikshay/NikshayBeneficiarySyncStatusRepo.java create mode 100644 src/main/java/com/iemr/mmu/service/nikshay/NikshaySyncStatusService.java create mode 100644 src/main/java/com/iemr/mmu/service/nikshay/impl/NikshaySyncStatusServiceImpl.java create mode 100644 src/main/java/com/iemr/mmu/utils/NikshayConstants.java diff --git a/src/main/java/com/iemr/mmu/controller/nikshay/NikshaySyncStatusController.java b/src/main/java/com/iemr/mmu/controller/nikshay/NikshaySyncStatusController.java new file mode 100644 index 00000000..20209c7b --- /dev/null +++ b/src/main/java/com/iemr/mmu/controller/nikshay/NikshaySyncStatusController.java @@ -0,0 +1,184 @@ +package com.iemr.mmu.controller.nikshay; + +import com.google.gson.Gson; +import com.iemr.mmu.data.nikshay.NikshayMissingBeneficiaryDTO; +import com.iemr.mmu.data.nikshay.NikshaySyncStatusUpdateDTO; +import com.iemr.mmu.service.nikshay.NikshaySyncStatusService; +import com.iemr.mmu.utils.NikshayConstants; +import com.iemr.mmu.utils.exception.IEMRException; +import com.iemr.mmu.utils.response.OutputResponse; +import com.iemr.mmu.utils.validator.Validator; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.Map; + +@RestController +@RequestMapping(NikshayConstants.NIKSHAY_PATH) +public class NikshaySyncStatusController { + + private static final int MAX_PAGE_SIZE = 100; + + private final Logger logger = LoggerFactory.getLogger(NikshaySyncStatusController.class); + + private final Gson gson = new Gson(); + + @Autowired + private NikshaySyncStatusService nikshaySyncStatusService; + + @Autowired + private Validator validator; + + @PostMapping("/syncStatus/syncFailed") + @Operation(summary = "Copy all FAILED rows from nikshay_beneficiary_phase_status into nikshay_beneficiary_sync_status") + public String syncFailedPhaseStatuses(@RequestHeader(value = "Authorization") String authorization, + @RequestHeader(value = "ServerAuthorization") String serverAuthorization) { + OutputResponse response = new OutputResponse(); + try { + Map result = nikshaySyncStatusService.syncFailedPhaseStatuses(serverAuthorization); + response.setResponse(gson.toJson(result)); + } catch (Exception e) { + logger.error("Error in syncFailedPhaseStatuses: " + e); + response.setError(5000, "Error syncing failed Nikshay phase statuses: " + e.getMessage()); + } + return response.toString(); + } + + @GetMapping("/syncStatus/missingBeneficiaries") + @Operation(summary = "Get beneficiaries whose Ni-kshay sync failed (paged); all of them with no filter, " + + "else only those matching providerServiceMapID and/or villageID") + public ResponseEntity> getMissingBeneficiaries( + @RequestParam(value = "providerServiceMapID", required = false) Integer providerServiceMapID, + @RequestParam(value = "villageID", required = false) Integer villageID, + @RequestParam(value = "pageNo", defaultValue = "0") Integer pageNo, + @RequestParam(value = "pageSize", defaultValue = "30") Integer pageSize) { + logger.info("getMissingBeneficiaries request: providerServiceMapID={}, villageID={}, pageNo={}, pageSize={}", + providerServiceMapID, villageID, pageNo, pageSize); + try { + if (pageNo < 0) { + return errorResponse(HttpStatus.BAD_REQUEST, "pageNo must be 0 or greater"); + } + if (pageSize < 1 || pageSize > MAX_PAGE_SIZE) { + return errorResponse(HttpStatus.BAD_REQUEST, "pageSize must be between 1 and " + MAX_PAGE_SIZE); + } + + Page page = nikshaySyncStatusService.getMissingBeneficiaries( + providerServiceMapID, villageID, pageNo, pageSize); + + Map pagination = new LinkedHashMap<>(); + pagination.put("page", page.getNumber()); + pagination.put("size", page.getSize()); + pagination.put("totalElements", page.getTotalElements()); + pagination.put("totalPages", page.getTotalPages()); + pagination.put("hasNext", page.hasNext()); + + Map body = new LinkedHashMap<>(); + body.put("status", "SUCCESS"); + body.put("message", page.isEmpty() + ? "No missing beneficiary data found" + : "Missing beneficiary data fetched successfully"); + body.put("data", page.getContent()); + body.put("pagination", pagination); + return ResponseEntity.ok(body); + } catch (Exception e) { + logger.error("Error in getMissingBeneficiaries for providerServiceMapID={}, villageID={}", + providerServiceMapID, villageID, e); + return errorResponse(HttpStatus.INTERNAL_SERVER_ERROR, + "Error fetching missing beneficiary data: " + e.getMessage()); + } + } + + @GetMapping(NikshayConstants.FAILED_BENEFICIARIES_PATH) + @Operation(summary = "Get every beneficiary with a FAILED phase in nikshay_beneficiary_phase_status, with all its phases (not paged)") + public ResponseEntity> getFailedBeneficiaries( + @RequestHeader(value = "ServerAuthorization") String serverAuthorization, HttpServletRequest request) { + // The JWT filter lets java/ and okhttp clients through without a token, and HttpInterceptor skips + // an empty Authorization header, so the header's presence proves nothing. ServerAuthorization must + // be a live login session key (the one /syncFailed sends after the data-sync server login). + if (serverAuthorization == null || serverAuthorization.trim().isEmpty()) { + return errorResponse(HttpStatus.UNAUTHORIZED, "ServerAuthorization is required"); + } + String remoteAddress = request.getHeader("X-FORWARDED-FOR"); + if (remoteAddress == null || remoteAddress.trim().isEmpty()) { + remoteAddress = request.getRemoteAddr(); + } + try { + validator.checkKeyExists(serverAuthorization, remoteAddress); + } catch (IEMRException e) { + return errorResponse(HttpStatus.UNAUTHORIZED, "Invalid or expired ServerAuthorization"); + } + + try { + return ResponseEntity.ok(nikshaySyncStatusService.getFailedBeneficiaries()); + } catch (Exception e) { + logger.error("Error in getFailedBeneficiaries", e); + return errorResponse(HttpStatus.INTERNAL_SERVER_ERROR, + "Error fetching failed Nikshay beneficiaries: " + e.getMessage()); + } + } + + @PostMapping("/syncStatus/update") + @Operation(summary = "Set isUpdated on all nikshay_beneficiary_sync_status rows of a beneficiary") + public ResponseEntity> updateSyncStatus(@RequestBody NikshaySyncStatusUpdateDTO request) { + logger.info("updateSyncStatus request: beneficiaryId={}, isUpdated={}", + request != null ? request.getBeneficiaryId() : null, + request != null ? request.getIsUpdated() : null); + try { + if (request == null || request.getBeneficiaryId() == null) { + return errorResponse(HttpStatus.BAD_REQUEST, "beneficiaryId is required"); + } + if (request.getIsUpdated() == null) { + return errorResponse(HttpStatus.BAD_REQUEST, "isUpdated is required"); + } + + int rows = nikshaySyncStatusService.updateIsUpdatedByBeneficiaryId( + request.getBeneficiaryId(), request.getIsUpdated()); + if (rows == 0) { + return errorResponse(HttpStatus.NOT_FOUND, + "No sync status record found for beneficiaryId " + request.getBeneficiaryId()); + } + + Map data = new LinkedHashMap<>(); + data.put("beneficiaryId", request.getBeneficiaryId()); + data.put("isUpdated", request.getIsUpdated()); + data.put("recordsUpdated", rows); + + Map body = new LinkedHashMap<>(); + body.put("status", "SUCCESS"); + body.put("message", "Update done for beneficiaryId " + request.getBeneficiaryId()); + body.put("data", data); + return ResponseEntity.ok(body); + } catch (Exception e) { + logger.error("Error in updateSyncStatus for beneficiaryId={}", + request != null ? request.getBeneficiaryId() : null, e); + return errorResponse(HttpStatus.INTERNAL_SERVER_ERROR, + "Error updating Nikshay sync status: " + e.getMessage()); + } + } + + private ResponseEntity> errorResponse(HttpStatus httpStatus, String message) { + if (httpStatus.is4xxClientError()) { + logger.warn("Nikshay sync status request rejected ({}): {}", httpStatus.value(), message); + } + Map body = new LinkedHashMap<>(); + body.put("status", "FAILED"); + body.put("message", message); + body.put("data", new ArrayList<>()); + return ResponseEntity.status(httpStatus).body(body); + } +} diff --git a/src/main/java/com/iemr/mmu/data/nikshay/BeneficiaryNameRow.java b/src/main/java/com/iemr/mmu/data/nikshay/BeneficiaryNameRow.java new file mode 100644 index 00000000..3efa2059 --- /dev/null +++ b/src/main/java/com/iemr/mmu/data/nikshay/BeneficiaryNameRow.java @@ -0,0 +1,11 @@ +package com.iemr.mmu.data.nikshay; + +// One row of NikshayBeneficiarySyncStatusRepo.findBeneficiaryNames; column aliases match the getters +public interface BeneficiaryNameRow { + + Number getBenRegID(); + + String getFirstName(); + + String getLastName(); +} diff --git a/src/main/java/com/iemr/mmu/data/nikshay/Beneficiarydetail.java b/src/main/java/com/iemr/mmu/data/nikshay/Beneficiarydetail.java new file mode 100644 index 00000000..2aded129 --- /dev/null +++ b/src/main/java/com/iemr/mmu/data/nikshay/Beneficiarydetail.java @@ -0,0 +1,231 @@ +package com.iemr.mmu.data.nikshay; + +import com.google.gson.annotations.Expose; +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 jakarta.persistence.Transient; +import lombok.Data; + +import java.math.BigInteger; +import java.sql.Timestamp; + +@Entity +@Table(name = "i_beneficiarydetails", schema = "db_identity") +@Data +public class Beneficiarydetail { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(unique = true, nullable = false) + private BigInteger beneficiaryDetailsId; + + private Integer areaId; + + @Column(name = "beneficiaryRegID") + private BigInteger BenRegId; + + @Column(length = 45) + private String community; + + @Column(nullable = false, length = 50, updatable = false) + private String createdBy; + + @Column(name = "CreatedDate") + private Timestamp createdDate; + + @Column(name = "dob") + private Timestamp dob; + + private Boolean deleted = false; + + @Column(length = 45) + private String education; + + private Boolean emergencyRegistration; + + @Column(length = 50) + private String fatherName; + + @Column(length = 20) + private String firstName; + + @Column(length = 50) + private String motherName; + + private String gender; + + @Column(name = "incomeStatusId") + private Integer incomeStatusId; + + @Column(length = 45) + private String incomeStatus; + + private Timestamp lastModDate; + + @Column(length = 20) + private String lastName; + + @Column(length = 20) + private String maritalstatus; + + @Column(name = "marriageDate") + private Timestamp marriageDate; + + @Transient + private Integer ageAtMarriage; + + @Column(name = "MiddleName", length = 20) + private String middleName; + + @Column(length = 50) + private String modifiedBy; + + private Integer occupationId; + + @Column(length = 45) + private String occupation; + + private Integer phcId; + + @Column(length = 30) + private String placeOfWork; + + private String literacyStatus; + + private Integer preferredLanguageId; + + @Column(length = 45) + private String preferredLanguage; + + @Column(nullable = false, length = 4) + private String processed = "N"; + + @Column(length = 45) + private BigInteger religionID; + + @Column(length = 45) + private String religion; + + @Column(length = 300) + private String remarks; + + private Boolean reserved; + + private Integer reservedById; + + @Column(length = 45) + private String reservedFor; + + private Timestamp reservedOn; + + private BigInteger servicePointId; + + @Column(length = 45) + private String sourceOfInfo; + + @Column(length = 50) + private String spousename; + + @Column(length = 10) + private String status; + + @Column(length = 10) + private String title; + + private Integer zoneId; + + @Column(name = "GenderID") + private Integer genderId; + + @Column(name = "MaritalStatusID") + private Integer maritalstatusId; + + @Column(name = "TitleID") + private Integer titleId; + + @Column(name = "CommunityID") + private Integer communityId; + + @Column(name = "EducationID") + private Integer educationId; + + @Column(name = "SexualOrientationID") + private Integer sexualOrientationID; + + @Column(name = "SexualOrientationType") + private String sexualOrientationType; + + @Column(name = "HIVStatus") + private Integer isHIVPositive; + + @Column(name = "HealthCareWorkerId") + private Integer healthCareWorkerId; + + private String healthCareWorker; + + @Expose + @Column(name = "vanID", updatable = false) + private Integer VanID; + @Expose + @Column(name = "parkingPlaceID", updatable = false) + private Integer parkingPlaceID; + + @Expose + @Column(name = "VanSerialNo", updatable = false) + private BigInteger id; + + @Expose + @Column(name = "SyncedBy") + private String SyncedBy; + + @Expose + @Column(name = "SyncedDate") + private Timestamp SyncedDate; + + @Transient + private BigInteger benficieryid; + + @Expose + @Transient + private Integer ProviderServiceMapID; + + @Expose + @Column(name = "familyid") + private String familyId; + + @Column(name = "ExtraFields") + private String otherFields; + + @Expose + @Column(name = "economicStatus") + private String economicStatus; + + @Expose + @Column(name = "economicStatusId") + private Integer economicStatusId; + + @Expose + @Column(name = "residentialArea") + private String residentialArea; + + @Expose + @Column(name = "residentialAreaId") + private Integer residentialAreaId; + + @Expose + @Column(name = "address") + private String address; + + @Expose + private String placeOfCurrentLiving; + + @Expose + private String otherPlaceOfCurrentLiving; + + @Expose + private String institutionName; + +} diff --git a/src/main/java/com/iemr/mmu/data/nikshay/NikshayBeneficiaryPhaseStatus.java b/src/main/java/com/iemr/mmu/data/nikshay/NikshayBeneficiaryPhaseStatus.java new file mode 100644 index 00000000..b5b8a48f --- /dev/null +++ b/src/main/java/com/iemr/mmu/data/nikshay/NikshayBeneficiaryPhaseStatus.java @@ -0,0 +1,75 @@ +package com.iemr.mmu.data.nikshay; + +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; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.sql.Timestamp; + +@Entity +@Table(name = "nikshay_beneficiary_phase_status", schema = "db_iemr") +@Data +public class NikshayBeneficiaryPhaseStatus { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "ben_reg_id", nullable = false) + private Long benRegID; + + // BeneficiaryID from db_identity.m_beneficiaryregidmapping; null if not found + @Column(name = "beneficiary_id") + private Long beneficiaryID; + + // INDEX | CONTACT | ALL_NEGATIVE + @Column(name = "beneficiary_type", length = 20, nullable = false) + private String beneficiaryType; + + // For a CONTACT: BenRegId of their index patient + @Column(name = "index_ben_reg_id") + private Long indexBenRegID; + + // ENROLMENT | CONTACTS | XRAY_UPLOAD | NAAT_UPLOAD | TRANSFER_OUT + @Column(name = "phase", length = 20, nullable = false) + private String phase; + + // PENDING | IN_PROGRESS | DONE | FAILED | NOT_APPLICABLE + @Column(name = "status", length = 20, nullable = false) + private String status; + + @Column(name = "failure_reason", length = 50) + private String failureReason; + + @Column(name = "error_detail", length = 2000) + private String errorDetail; + + @Column(name = "attempts", nullable = false) + private Integer attempts = 0; + + @Column(name = "nikshay_id", length = 50) + private String nikshayId; + + @Column(name = "episode_id", length = 50) + private String episodeId; + + @Column(name = "last_attempt_at") + private Timestamp lastAttemptAt; + + @Column(name = "completed_at") + private Timestamp completedAt; + + @CreationTimestamp + @Column(name = "created_at", updatable = false) + private Timestamp createdAt; + + @UpdateTimestamp + @Column(name = "updated_at") + private Timestamp updatedAt; +} diff --git a/src/main/java/com/iemr/mmu/data/nikshay/NikshayBeneficiarySyncStatus.java b/src/main/java/com/iemr/mmu/data/nikshay/NikshayBeneficiarySyncStatus.java new file mode 100644 index 00000000..ff72cd23 --- /dev/null +++ b/src/main/java/com/iemr/mmu/data/nikshay/NikshayBeneficiarySyncStatus.java @@ -0,0 +1,72 @@ +package com.iemr.mmu.data.nikshay; + +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; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.sql.Timestamp; + +@Entity +@Table(name = "nikshay_beneficiary_sync_status", schema = "db_iemr") +@Data +public class NikshayBeneficiarySyncStatus { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "ben_reg_id", nullable = false) + private Long benRegID; + + // BeneficiaryID from db_identity.m_beneficiaryregidmapping; null if not found + @Column(name = "beneficiary_id") + private Long beneficiaryID; + + // INDEX | CONTACT | ALL_NEGATIVE + @Column(name = "beneficiary_type", length = 20, nullable = false) + private String beneficiaryType; + + // For a CONTACT: BenRegId of their index patient + @Column(name = "index_ben_reg_id") + private Long indexBenRegID; + + // ENROLMENT | CONTACTS | XRAY_UPLOAD | NAAT_UPLOAD | TRANSFER_OUT + @Column(name = "phase", length = 20, nullable = false) + private String phase; + + // PENDING | IN_PROGRESS | DONE | FAILED | NOT_APPLICABLE + @Column(name = "status", length = 20, nullable = false) + private String status; + + @Column(name = "failure_reason", length = 50) + private String failureReason; + + @Column(name = "error_detail", length = 2000) + private String errorDetail; + + @Column(name = "nikshay_id", length = 50) + private String nikshayId; + + @Column(name = "episode_id", length = 50) + private String episodeId; + + @Column(name = "last_attempt_at") + private Timestamp lastAttemptAt; + + @Column(name = "isUpdated", nullable = false) + private Boolean isUpdated = false; + + @CreationTimestamp + @Column(name = "created_at", updatable = false) + private Timestamp createdAt; + + @UpdateTimestamp + @Column(name = "updated_at") + private Timestamp updatedAt; +} diff --git a/src/main/java/com/iemr/mmu/data/nikshay/NikshayMissingBeneficiaryDTO.java b/src/main/java/com/iemr/mmu/data/nikshay/NikshayMissingBeneficiaryDTO.java new file mode 100644 index 00000000..037cd37f --- /dev/null +++ b/src/main/java/com/iemr/mmu/data/nikshay/NikshayMissingBeneficiaryDTO.java @@ -0,0 +1,27 @@ +package com.iemr.mmu.data.nikshay; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class NikshayMissingBeneficiaryDTO { + + private Long beneficiaryId; + + @JsonProperty("isUpdated") + private Boolean isUpdated; + + private List reason; + + private String name; + + private Integer villageID; + + private String villageName; +} diff --git a/src/main/java/com/iemr/mmu/data/nikshay/NikshayMissingBeneficiaryRow.java b/src/main/java/com/iemr/mmu/data/nikshay/NikshayMissingBeneficiaryRow.java new file mode 100644 index 00000000..345c8e4f --- /dev/null +++ b/src/main/java/com/iemr/mmu/data/nikshay/NikshayMissingBeneficiaryRow.java @@ -0,0 +1,15 @@ +package com.iemr.mmu.data.nikshay; + +// One row of NikshayBeneficiarySyncStatusRepo.findMissingBeneficiaries; column aliases match the getters +public interface NikshayMissingBeneficiaryRow { + + Number getBenRegID(); + + Number getBeneficiaryID(); + + String getName(); + + Number getVillageID(); + + String getVillageName(); +} diff --git a/src/main/java/com/iemr/mmu/data/nikshay/NikshaySyncStatusUpdateDTO.java b/src/main/java/com/iemr/mmu/data/nikshay/NikshaySyncStatusUpdateDTO.java new file mode 100644 index 00000000..4ae0ce0a --- /dev/null +++ b/src/main/java/com/iemr/mmu/data/nikshay/NikshaySyncStatusUpdateDTO.java @@ -0,0 +1,13 @@ +package com.iemr.mmu.data.nikshay; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class NikshaySyncStatusUpdateDTO { + + private Long beneficiaryId; + + @JsonProperty("isUpdated") + private Boolean isUpdated; +} diff --git a/src/main/java/com/iemr/mmu/repo/nikshay/BeneficiaryRepo.java b/src/main/java/com/iemr/mmu/repo/nikshay/BeneficiaryRepo.java new file mode 100644 index 00000000..24bf80d8 --- /dev/null +++ b/src/main/java/com/iemr/mmu/repo/nikshay/BeneficiaryRepo.java @@ -0,0 +1,20 @@ +package com.iemr.mmu.repo.nikshay; + + +import com.iemr.mmu.data.nikshay.Beneficiarydetail; +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.math.BigInteger; +import java.util.Collection; +import java.util.List; + +@Repository +public interface BeneficiaryRepo extends JpaRepository { + + @Query("SELECT d FROM Beneficiarydetail d WHERE d.BenRegId IN :benRegIDs") + List findDetailsByBenRegIDIn(@Param("benRegIDs") Collection benRegIDs); + +} diff --git a/src/main/java/com/iemr/mmu/repo/nikshay/NikshayBeneficiaryPhaseStatusRepo.java b/src/main/java/com/iemr/mmu/repo/nikshay/NikshayBeneficiaryPhaseStatusRepo.java new file mode 100644 index 00000000..f2689842 --- /dev/null +++ b/src/main/java/com/iemr/mmu/repo/nikshay/NikshayBeneficiaryPhaseStatusRepo.java @@ -0,0 +1,23 @@ +package com.iemr.mmu.repo.nikshay; + +import com.iemr.mmu.data.nikshay.NikshayBeneficiaryPhaseStatus; +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.Collection; +import java.util.List; + +@Repository +public interface NikshayBeneficiaryPhaseStatusRepo extends JpaRepository { + + List findByBenRegIDIn(Collection benRegIDs); + + // Every phase row (any status) of every beneficiary that has at least one phase in the given status, + // newest beneficiary first + @Query("SELECT p FROM NikshayBeneficiaryPhaseStatus p WHERE p.benRegID IN " + + "(SELECT f.benRegID FROM NikshayBeneficiaryPhaseStatus f WHERE f.status = :status) " + + "ORDER BY p.benRegID DESC") + List findAllPhasesOfBeneficiariesWithStatus(@Param("status") String status); +} diff --git a/src/main/java/com/iemr/mmu/repo/nikshay/NikshayBeneficiarySyncStatusRepo.java b/src/main/java/com/iemr/mmu/repo/nikshay/NikshayBeneficiarySyncStatusRepo.java new file mode 100644 index 00000000..236ceacf --- /dev/null +++ b/src/main/java/com/iemr/mmu/repo/nikshay/NikshayBeneficiarySyncStatusRepo.java @@ -0,0 +1,87 @@ +package com.iemr.mmu.repo.nikshay; + +import com.iemr.mmu.data.nikshay.BeneficiaryNameRow; +import com.iemr.mmu.data.nikshay.NikshayBeneficiarySyncStatus; +import com.iemr.mmu.data.nikshay.NikshayMissingBeneficiaryRow; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import java.sql.Timestamp; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +@Repository +public interface NikshayBeneficiarySyncStatusRepo extends JpaRepository { + + Optional findByBenRegIDAndPhase(Long benRegID, String phase); + + + List findByBenRegIDIn(Collection benRegIDs); + + // updated_at is refreshed by the column's ON UPDATE CURRENT_TIMESTAMP + @Transactional + @Modifying + @Query("UPDATE NikshayBeneficiarySyncStatus s SET s.isUpdated = :isUpdated WHERE s.beneficiaryID = :beneficiaryID") + int updateIsUpdatedByBeneficiaryID(@Param("beneficiaryID") Long beneficiaryID, @Param("isUpdated") Boolean isUpdated); + + + // Purges rows already marked updated whose last change is older than the cutoff + @Transactional + @Modifying + @Query("DELETE FROM NikshayBeneficiarySyncStatus s WHERE s.isUpdated = true " + + "AND COALESCE(s.updatedAt, s.createdAt) < :cutoff") + int deleteUpdatedOlderThan(@Param("cutoff") Timestamp cutoff); + + // Shared FROM/WHERE of findMissingBeneficiaries and its count query (annotation values must be constants). + // Identity tables are LEFT JOINed so that, with no filter, every beneficiary in the sync-status table + // is returned even when its identity data is missing on this server. + // - villageId: matched on the beneficiary's current address village (i_beneficiaryaddress.CurrVillageId) + // - psmId: matched on i_beneficiarydetails_rmnch.ProviderServiceMapID + // When both are given, both must match. + String MISSING_BENEFICIARIES_FROM = + "FROM (SELECT DISTINCT ben_reg_id FROM db_iemr.nikshay_beneficiary_sync_status) s " + + "LEFT JOIN db_identity.i_beneficiarymapping bm ON bm.BenRegId = s.ben_reg_id AND bm.Deleted = 0 " + + "LEFT JOIN db_identity.i_beneficiarydetails d ON d.BeneficiaryDetailsId = bm.BenDetailsId " + + "AND (d.Deleted IS NULL OR d.Deleted = 0) " + + "LEFT JOIN db_identity.i_beneficiaryaddress a ON a.BenAddressID = bm.BenAddressId "; + + String MISSING_BENEFICIARIES_WHERE = + "WHERE (:villageId IS NULL OR (d.BeneficiaryDetailsId IS NOT NULL " + + "AND a.CurrVillageId = :villageId)) " + + "AND (:psmId IS NULL OR EXISTS (SELECT 1 FROM db_identity.i_beneficiarydetails_rmnch r " + + "WHERE r.BeneficiaryRegID = s.ben_reg_id AND r.ProviderServiceMapID = :psmId " + + "AND (r.Deleted IS NULL OR r.Deleted = 0)))"; + + // Beneficiaries that have a row in nikshay_beneficiary_sync_status, one entry per beneficiary even + // with a row per phase. No filter returns all of them; a filter keeps only the matching ones. + // Native SQL because the identity tables live in db_identity, outside this persistence unit. + @Query(nativeQuery = true, + value = "SELECT s.ben_reg_id AS benRegID, rg.BeneficiaryID AS beneficiaryID, " + + "NULLIF(TRIM(CONCAT_WS(' ', d.FirstName, d.LastName)), '') AS name, " + + "a.CurrVillageId AS villageID, " + + "a.CurrVillage AS villageName " + + MISSING_BENEFICIARIES_FROM + + "LEFT JOIN db_identity.m_beneficiaryregidmapping rg ON rg.BenRegId = s.ben_reg_id " + + MISSING_BENEFICIARIES_WHERE + + " ORDER BY s.ben_reg_id DESC", + countQuery = "SELECT COUNT(*) " + MISSING_BENEFICIARIES_FROM + MISSING_BENEFICIARIES_WHERE) + Page findMissingBeneficiaries(@Param("psmId") Integer psmId, + @Param("villageId") Integer villageId, + Pageable pageable); + + // Names of the given beneficiaries from i_beneficiarydetails, deleted records excluded. + // Native projection of only the needed columns: i_beneficiarydetails is not mapped as an entity, + // so a column added, renamed or retyped elsewhere in that table cannot break this lookup. + @Query(nativeQuery = true, + value = "SELECT d.BeneficiaryRegID AS benRegID, d.FirstName AS firstName, d.LastName AS lastName " + + "FROM db_identity.i_beneficiarydetails d " + + "WHERE d.BeneficiaryRegID IN (:benRegIDs) AND (d.Deleted IS NULL OR d.Deleted = 0)") + List findBeneficiaryNames(@Param("benRegIDs") Collection benRegIDs); +} diff --git a/src/main/java/com/iemr/mmu/service/nikshay/NikshaySyncStatusService.java b/src/main/java/com/iemr/mmu/service/nikshay/NikshaySyncStatusService.java new file mode 100644 index 00000000..4d7e223b --- /dev/null +++ b/src/main/java/com/iemr/mmu/service/nikshay/NikshaySyncStatusService.java @@ -0,0 +1,18 @@ +package com.iemr.mmu.service.nikshay; + +import com.iemr.mmu.data.nikshay.NikshayMissingBeneficiaryDTO; +import org.springframework.data.domain.Page; + +import java.util.Map; + +public interface NikshaySyncStatusService { + + Map syncFailedPhaseStatuses(String serverAuthorization) throws Exception; + + Page getMissingBeneficiaries(Integer providerServiceMapID, Integer villageID, + int pageNo, int pageSize); + + Map getFailedBeneficiaries(); + + int updateIsUpdatedByBeneficiaryId(Long beneficiaryId, Boolean isUpdated); +} diff --git a/src/main/java/com/iemr/mmu/service/nikshay/impl/NikshaySyncStatusServiceImpl.java b/src/main/java/com/iemr/mmu/service/nikshay/impl/NikshaySyncStatusServiceImpl.java new file mode 100644 index 00000000..e4e155f7 --- /dev/null +++ b/src/main/java/com/iemr/mmu/service/nikshay/impl/NikshaySyncStatusServiceImpl.java @@ -0,0 +1,360 @@ +package com.iemr.mmu.service.nikshay.impl; + + +import com.iemr.mmu.data.nikshay.NikshayBeneficiaryPhaseStatus; +import com.iemr.mmu.data.nikshay.NikshayBeneficiarySyncStatus; +import com.iemr.mmu.data.nikshay.NikshayMissingBeneficiaryDTO; +import com.iemr.mmu.data.nikshay.NikshayMissingBeneficiaryRow; +import com.iemr.mmu.data.nikshay.BeneficiaryNameRow; +import com.iemr.mmu.repo.nikshay.NikshayBeneficiaryPhaseStatusRepo; +import com.iemr.mmu.repo.nikshay.NikshayBeneficiarySyncStatusRepo; +import com.iemr.mmu.service.nikshay.NikshaySyncStatusService; +import com.iemr.mmu.utils.NikshayConstants; +import jakarta.annotation.PostConstruct; +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.HttpStatusCodeException; +import org.springframework.web.client.RestTemplate; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Service +public class NikshaySyncStatusServiceImpl implements NikshaySyncStatusService { + + private static final String STATUS_FAILED = "FAILED"; + + private static final String STATUS_NOT_STARTED = "NOT_STARTED"; + + private static final String PHASE_ENROLMENT = "ENROLMENT"; + + // Every beneficiary is reported with all phases, in this order + private static final List PHASES = + List.of(PHASE_ENROLMENT, "NAAT_UPLOAD", "XRAY_UPLOAD", "CONTACTS", "TRANSFER_OUT"); + + private final Logger logger = LoggerFactory.getLogger(NikshaySyncStatusServiceImpl.class); + + @Autowired + private NikshayBeneficiaryPhaseStatusRepo phaseStatusRepo; + + @Autowired + private NikshayBeneficiarySyncStatusRepo syncStatusRepo; + + @Value("${nikshay.http.connectTimeoutMs:15000}") + private int connectTimeoutMs; + + // the failedBeneficiaries API is not paged, so allow it time on a large backlog + @Value("${nikshay.http.readTimeoutMs:120000}") + private int readTimeoutMs; + + private RestTemplate restTemplate; + + @PostConstruct + public void init() { + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setConnectTimeout(connectTimeoutMs); + factory.setReadTimeout(readTimeoutMs); + restTemplate = new RestTemplate(factory); + } + + // Fetches the FAILED phases from the failedBeneficiaries API (deployed on another server), then purges + // rows already marked updated more than a month ago and stores one row per FAILED phase. + // (ben_reg_id, phase) is unique, so a surviving row is refreshed in place and keeps its isUpdated + // flag; a new row starts with isUpdated = false. + @Override + @Transactional + public Map syncFailedPhaseStatuses(String serverAuthorization) throws Exception { + // fetched before touching the table, so an unreachable server leaves it as it was + List failed = fetchFailedPhases(serverAuthorization); + + Timestamp cutoff = Timestamp.valueOf(LocalDateTime.now().minusMonths(1)); + int deleted = syncStatusRepo.deleteUpdatedOlderThan(cutoff); + logger.info("Nikshay sync: deleted {} rows with isUpdated = true last changed before {}", deleted, cutoff); + + int inserted = 0; + int updated = 0; + List toSave = new ArrayList<>(); + for (NikshayBeneficiarySyncStatus remote : failed) { + NikshayBeneficiarySyncStatus sync = syncStatusRepo + .findByBenRegIDAndPhase(remote.getBenRegID(), remote.getPhase()) + .orElse(null); + if (sync == null) { + sync = new NikshayBeneficiarySyncStatus(); + inserted++; + } else { + updated++; + } + sync.setBenRegID(remote.getBenRegID()); + sync.setBeneficiaryID(remote.getBeneficiaryID()); + sync.setBeneficiaryType(remote.getBeneficiaryType()); + sync.setIndexBenRegID(remote.getIndexBenRegID()); + sync.setPhase(remote.getPhase()); + sync.setStatus(remote.getStatus()); + sync.setFailureReason(remote.getFailureReason()); + sync.setErrorDetail(remote.getErrorDetail()); + sync.setNikshayId(remote.getNikshayId()); + sync.setEpisodeId(remote.getEpisodeId()); + sync.setLastAttemptAt(remote.getLastAttemptAt()); + toSave.add(sync); + } + syncStatusRepo.saveAll(toSave); + + logger.info("Nikshay sync: {} FAILED phase rows received, {} inserted, {} updated", + failed.size(), inserted, updated); + + Map result = new HashMap<>(); + result.put("failedRecordsFound", failed.size()); + result.put("deleted", deleted); + result.put("inserted", inserted); + result.put("updated", updated); + return result; + } + + @Override + @Transactional(readOnly = true) + public Page getMissingBeneficiaries(Integer providerServiceMapID, Integer villageID, + int pageNo, int pageSize) { + // beneficiary, name and village come from the AMRIT identity tables (see the repo query) + Page benPage = syncStatusRepo.findMissingBeneficiaries( + providerServiceMapID, villageID, PageRequest.of(pageNo, pageSize)); + List benRegIDs = benPage.getContent().stream() + .map(row -> row.getBenRegID().longValue()) + .collect(Collectors.toList()); + logger.info("Nikshay missing beneficiaries: psmId={}, villageId={}, page={}, size={} -> {} on page, {} total", + providerServiceMapID, villageID, pageNo, pageSize, benRegIDs.size(), benPage.getTotalElements()); + if (benRegIDs.isEmpty()) { + return new PageImpl<>(new ArrayList<>(), benPage.getPageable(), benPage.getTotalElements()); + } + + Map> syncRowsByBen = new HashMap<>(); + for (NikshayBeneficiarySyncStatus row : syncStatusRepo.findByBenRegIDIn(benRegIDs)) { + syncRowsByBen.computeIfAbsent(row.getBenRegID(), k -> new ArrayList<>()).add(row); + } + + List data = new ArrayList<>(); + for (NikshayMissingBeneficiaryRow ben : benPage.getContent()) { + Long benRegID = ben.getBenRegID().longValue(); + List rows = syncRowsByBen.getOrDefault(benRegID, new ArrayList<>()); + + Long beneficiaryID = null; + boolean isUpdated = !rows.isEmpty(); + Set reasons = new LinkedHashSet<>(); + for (NikshayBeneficiarySyncStatus row : rows) { + if (beneficiaryID == null) beneficiaryID = row.getBeneficiaryID(); + // a beneficiary counts as updated only once every failed phase has been updated + if (!Boolean.TRUE.equals(row.getIsUpdated())) isUpdated = false; + reasons.add(row.getFailureReason() != null + ? row.getFailureReason() + : row.getPhase() + " sync failed"); + } + if (beneficiaryID == null && ben.getBeneficiaryID() != null) { + beneficiaryID = ben.getBeneficiaryID().longValue(); + } + + data.add(new NikshayMissingBeneficiaryDTO(beneficiaryID, isUpdated, new ArrayList<>(reasons), + ben.getName(), + ben.getVillageID() != null ? ben.getVillageID().intValue() : null, + ben.getVillageName())); + } + return new PageImpl<>(data, benPage.getPageable(), benPage.getTotalElements()); + } + + // Every beneficiary with at least one FAILED phase, with only its FAILED phases. Not paged. + @Override + @Transactional(readOnly = true) + public Map getFailedBeneficiaries() { + // one query: all phases (the ENROLMENT row carries the nikshayId even when it did not fail), + // ordered newest beneficiary first; the LinkedHashMap keeps that order + Map> phasesByBen = new LinkedHashMap<>(); + Set indexBenRegIDs = new LinkedHashSet<>(); + for (NikshayBeneficiaryPhaseStatus row : phaseStatusRepo.findAllPhasesOfBeneficiariesWithStatus(STATUS_FAILED)) { + phasesByBen.computeIfAbsent(row.getBenRegID(), k -> new HashMap<>()).put(row.getPhase(), row); + if (row.getIndexBenRegID() != null) indexBenRegIDs.add(row.getIndexBenRegID()); + } + List benRegIDs = new ArrayList<>(phasesByBen.keySet()); + logger.info("Nikshay failed beneficiaries: {} beneficiaries with a FAILED phase", benRegIDs.size()); + + List> beneficiaries = new ArrayList<>(); + if (!benRegIDs.isEmpty()) { + Set nameBenRegIDs = new LinkedHashSet<>(benRegIDs); + nameBenRegIDs.addAll(indexBenRegIDs); + + // Names of the beneficiaries and of their index patients, from i_beneficiarydetails + // (deleted records are already excluded by the query) + Map nameByBen = new HashMap<>(); + for (BeneficiaryNameRow detail : syncStatusRepo.findBeneficiaryNames(nameBenRegIDs)) { + if (detail.getBenRegID() == null) continue; + nameByBen.putIfAbsent(detail.getBenRegID().longValue(), buildName(detail)); + } + + for (Long benRegID : benRegIDs) { + Map rows = phasesByBen.getOrDefault(benRegID, new HashMap<>()); + // beneficiary-level fields are the same on every phase row; prefer the ENROLMENT row + NikshayBeneficiaryPhaseStatus first = rows.containsKey(PHASE_ENROLMENT) + ? rows.get(PHASE_ENROLMENT) + : rows.values().stream().findFirst().orElse(null); + Long indexBenRegID = first != null ? first.getIndexBenRegID() : null; + NikshayBeneficiaryPhaseStatus enrolment = rows.get(PHASE_ENROLMENT); + + Map phases = new LinkedHashMap<>(); + for (String phase : PHASES) { + NikshayBeneficiaryPhaseStatus row = rows.get(phase); + if (row != null && STATUS_FAILED.equals(row.getStatus())) { + phases.put(phase, toPhaseMap(row)); + } + } + + Map ben = new LinkedHashMap<>(); + ben.put("benRegId", benRegID); + ben.put("beneficiaryId", first != null ? first.getBeneficiaryID() : null); + ben.put("name", nameByBen.get(benRegID)); + ben.put("beneficiaryType", first != null ? first.getBeneficiaryType() : null); + ben.put("indexBenRegId", indexBenRegID); + ben.put("indexName", indexBenRegID != null ? nameByBen.get(indexBenRegID) : null); + ben.put("nikshayId", enrolment != null ? enrolment.getNikshayId() : null); + ben.put("phases", phases); + beneficiaries.add(ben); + } + } + + Map result = new LinkedHashMap<>(); + result.put("total", beneficiaries.size()); + result.put("beneficiaries", beneficiaries); + return result; + } + + // Updates every phase row of the beneficiary. Rows synced before beneficiary_id could be resolved + // hold it as null, so when nothing matches on beneficiary_id, fall back to the BenRegId. + @Override + public int updateIsUpdatedByBeneficiaryId(Long beneficiaryId, Boolean isUpdated) { + int rows = syncStatusRepo.updateIsUpdatedByBeneficiaryID(beneficiaryId, isUpdated); +// if (rows == 0) { +// Long benRegID = beneficiaryRepo.getBenRegIdFromBenId(beneficiaryId); +// if (benRegID != null) { +// logger.info("Nikshay sync status: no rows on beneficiaryId={}, retrying with benRegID={}", +// beneficiaryId, benRegID); +// rows = syncStatusRepo.updateIsUpdatedByBenRegID(benRegID, isUpdated); +// } +// } + logger.info("Nikshay sync status: set isUpdated={} for beneficiaryId={} on {} rows", + isUpdated, beneficiaryId, rows); + return rows; + } + + private Map toPhaseMap(NikshayBeneficiaryPhaseStatus row) { + Map phase = new LinkedHashMap<>(); + phase.put("status", row != null ? row.getStatus() : STATUS_NOT_STARTED); + phase.put("failureReason", row != null ? row.getFailureReason() : null); + phase.put("errorDetail", row != null ? row.getErrorDetail() : null); + phase.put("attempts", row != null && row.getAttempts() != null ? row.getAttempts() : 0); + phase.put("nikshayId", row != null ? row.getNikshayId() : null); + phase.put("episodeId", row != null ? row.getEpisodeId() : null); + phase.put("lastAttemptAt", row != null ? row.getLastAttemptAt() : null); + phase.put("completedAt", row != null ? row.getCompletedAt() : null); + return phase; + } + + // Calls the failedBeneficiaries API and flattens its response to one row per FAILED phase + private List fetchFailedPhases(String serverAuthorization) throws Exception { + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(List.of(MediaType.APPLICATION_JSON)); + // the JWT filter on the other server lets a java/ client through on its Authorization header; + // the controller itself requires ServerAuthorization + headers.set(HttpHeaders.AUTHORIZATION, serverAuthorization); + headers.set("ServerAuthorization", serverAuthorization); + + ResponseEntity response; + try { + response = restTemplate.exchange(NikshayConstants.FAILED_BENEFICIARIES_URL, HttpMethod.GET, new HttpEntity<>(headers), + String.class); + } catch (HttpStatusCodeException e) { + throw new Exception("failedBeneficiaries API returned " + e.getStatusCode().value() + ": " + + e.getResponseBodyAsString()); + } + if (!response.hasBody()) + throw new Exception("Empty response from failedBeneficiaries API"); + + JSONObject body = new JSONObject(response.getBody()); + if (!body.has("beneficiaries")) + // a rejected session comes back from HttpInterceptor as HTTP 200 with an OutputResponse body + throw new Exception("failedBeneficiaries API failed: " + + body.optString("errorMessage", body.optString("message"))); + + List rows = new ArrayList<>(); + JSONArray beneficiaries = body.getJSONArray("beneficiaries"); + for (int i = 0; i < beneficiaries.length(); i++) { + JSONObject ben = beneficiaries.getJSONObject(i); + JSONObject phases = ben.optJSONObject("phases"); + if (phases == null) continue; + for (String phaseName : phases.keySet()) { + JSONObject phase = phases.getJSONObject(phaseName); + if (!STATUS_FAILED.equals(phase.optString("status"))) continue; + + NikshayBeneficiarySyncStatus row = new NikshayBeneficiarySyncStatus(); + row.setBenRegID(ben.getLong("benRegId")); + row.setBeneficiaryID(optLong(ben, "beneficiaryId")); + row.setBeneficiaryType(ben.optString("beneficiaryType", null)); + row.setIndexBenRegID(optLong(ben, "indexBenRegId")); + row.setPhase(phaseName); + row.setStatus(STATUS_FAILED); + row.setFailureReason(optString(phase, "failureReason")); + row.setErrorDetail(optString(phase, "errorDetail")); + // the phase's own nikshayId, else the beneficiary's (from its ENROLMENT phase) + String nikshayId = optString(phase, "nikshayId"); + row.setNikshayId(nikshayId != null ? nikshayId : optString(ben, "nikshayId")); + row.setEpisodeId(optString(phase, "episodeId")); + row.setLastAttemptAt(optTimestamp(phase, "lastAttemptAt")); + rows.add(row); + } + } + logger.info("Nikshay sync: failedBeneficiaries API returned {} beneficiaries, {} FAILED phases", + beneficiaries.length(), rows.size()); + return rows; + } + + private static String optString(JSONObject obj, String key) { + return obj.isNull(key) ? null : obj.optString(key, null); + } + + private static Long optLong(JSONObject obj, String key) { + return obj.isNull(key) ? null : obj.getLong(key); + } + + // Spring serialises a Timestamp as an ISO string or as epoch millis, depending on its Jackson settings + private static Timestamp optTimestamp(JSONObject obj, String key) { + if (obj.isNull(key)) return null; + Object value = obj.get(key); + if (value instanceof Number) return new Timestamp(((Number) value).longValue()); + return Timestamp.from(OffsetDateTime.parse(value.toString()).toInstant()); + } + + private String buildName(BeneficiaryNameRow detail) { + String name = ((detail.getFirstName() != null ? detail.getFirstName() : "") + " " + + (detail.getLastName() != null ? detail.getLastName() : "")).trim(); + return name.isEmpty() ? null : name; + } +} diff --git a/src/main/java/com/iemr/mmu/utils/NikshayConstants.java b/src/main/java/com/iemr/mmu/utils/NikshayConstants.java new file mode 100644 index 00000000..3d477579 --- /dev/null +++ b/src/main/java/com/iemr/mmu/utils/NikshayConstants.java @@ -0,0 +1,16 @@ +package com.iemr.mmu.utils; + +public final class NikshayConstants { + + private NikshayConstants() { + } + + // Server that hosts the failedBeneficiaries API (it reads nikshay_beneficiary_phase_status) + public static final String BASE_URL = "http://10.208.122.38:8080/mmu-api"; + + public static final String NIKSHAY_PATH = "/nikshay"; + + public static final String FAILED_BENEFICIARIES_PATH = "/syncStatus/failedBeneficiaries"; + + public static final String FAILED_BENEFICIARIES_URL = BASE_URL + NIKSHAY_PATH + FAILED_BENEFICIARIES_PATH; +} From 9e63d93ebd839c6ac7e068d93ffadf99e54a141d Mon Sep 17 00:00:00 2001 From: Pankaj Kumar Date: Mon, 2 Nov 2026 18:35:11 +0530 Subject: [PATCH 2/7] changes suggested in PR review --- .../mmu/repo/nikshay/NikshayBeneficiarySyncStatusRepo.java | 2 +- .../service/nikshay/impl/NikshaySyncStatusServiceImpl.java | 7 ++++++- src/main/java/com/iemr/mmu/utils/NikshayConstants.java | 5 ----- src/main/resources/application.properties | 4 ++++ 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/iemr/mmu/repo/nikshay/NikshayBeneficiarySyncStatusRepo.java b/src/main/java/com/iemr/mmu/repo/nikshay/NikshayBeneficiarySyncStatusRepo.java index 236ceacf..eb4b55d7 100644 --- a/src/main/java/com/iemr/mmu/repo/nikshay/NikshayBeneficiarySyncStatusRepo.java +++ b/src/main/java/com/iemr/mmu/repo/nikshay/NikshayBeneficiarySyncStatusRepo.java @@ -36,7 +36,7 @@ public interface NikshayBeneficiarySyncStatusRepo extends JpaRepository fetchFailedPhases(String serverAuthor ResponseEntity response; try { - response = restTemplate.exchange(NikshayConstants.FAILED_BENEFICIARIES_URL, HttpMethod.GET, new HttpEntity<>(headers), + String url = nikshayBaseUrl + NikshayConstants.NIKSHAY_PATH + NikshayConstants.FAILED_BENEFICIARIES_PATH; + response = restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(headers), String.class); } catch (HttpStatusCodeException e) { throw new Exception("failedBeneficiaries API returned " + e.getStatusCode().value() + ": " diff --git a/src/main/java/com/iemr/mmu/utils/NikshayConstants.java b/src/main/java/com/iemr/mmu/utils/NikshayConstants.java index 3d477579..3698c08c 100644 --- a/src/main/java/com/iemr/mmu/utils/NikshayConstants.java +++ b/src/main/java/com/iemr/mmu/utils/NikshayConstants.java @@ -5,12 +5,7 @@ public final class NikshayConstants { private NikshayConstants() { } - // Server that hosts the failedBeneficiaries API (it reads nikshay_beneficiary_phase_status) - public static final String BASE_URL = "http://10.208.122.38:8080/mmu-api"; - public static final String NIKSHAY_PATH = "/nikshay"; public static final String FAILED_BENEFICIARIES_PATH = "/syncStatus/failedBeneficiaries"; - - public static final String FAILED_BENEFICIARIES_URL = BASE_URL + NIKSHAY_PATH + FAILED_BENEFICIARIES_PATH; } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d1f926a2..790b3d2b 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -61,3 +61,7 @@ spring.mvc.async.request-timeout=600000 # Undertow settings (the actual server in use - WildFly/JBoss) server.undertow.no-request-timeout=600000 server.undertow.max-http-post-size=-1 + +# ============= NIKSHAY ============= +# Server that hosts the failedBeneficiaries API +nikshay.baseUrl=http://10.208.122.38:8080/mmu-api From 0b961228ebf8f0e2d0d99e7c4e5c9f4f32a05e6c Mon Sep 17 00:00:00 2001 From: Pankaj Kumar Date: Tue, 3 Nov 2026 15:46:37 +0530 Subject: [PATCH 3/7] more changes as per the PR review suggestions --- src/main/environment/common_ci.properties | 5 ++++- src/main/environment/common_docker.properties | 5 ++++- .../mmu/repo/nikshay/NikshayBeneficiarySyncStatusRepo.java | 2 +- src/main/resources/application.properties | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main/environment/common_ci.properties b/src/main/environment/common_ci.properties index faf5180f..ffcafe41 100644 --- a/src/main/environment/common_ci.properties +++ b/src/main/environment/common_ci.properties @@ -95,4 +95,7 @@ logging.file.name=@env.MMU_API_LOGGING_FILE_NAME@ springdoc.api-docs.enabled=@env.SWAGGER_DOC_ENABLED@ springdoc.swagger-ui.enabled=@env.SWAGGER_DOC_ENABLED@ -cors.allowed-origins=@env.CORS_ALLOWED_ORIGINS@ \ No newline at end of file +cors.allowed-origins=@env.CORS_ALLOWED_ORIGINS@ + +# Server that hosts the failedBeneficiaries API +nikshay.baseUrl=@env.NIKSHAY_BASE_URL@ diff --git a/src/main/environment/common_docker.properties b/src/main/environment/common_docker.properties index 28f6b89e..23a68d0e 100644 --- a/src/main/environment/common_docker.properties +++ b/src/main/environment/common_docker.properties @@ -93,4 +93,7 @@ logging.path=logs logging.file.name=${MMU_API_LOGGING_FILE_NAME} springdoc.api-docs.enabled=${SWAGGER_DOC_ENABLED} -springdoc.swagger-ui.enabled=${SWAGGER_DOC_ENABLED} \ No newline at end of file +springdoc.swagger-ui.enabled=${SWAGGER_DOC_ENABLED} + +# Server that hosts the failedBeneficiaries API +nikshay.baseUrl=${NIKSHAY_BASE_URL} diff --git a/src/main/java/com/iemr/mmu/repo/nikshay/NikshayBeneficiarySyncStatusRepo.java b/src/main/java/com/iemr/mmu/repo/nikshay/NikshayBeneficiarySyncStatusRepo.java index eb4b55d7..236ceacf 100644 --- a/src/main/java/com/iemr/mmu/repo/nikshay/NikshayBeneficiarySyncStatusRepo.java +++ b/src/main/java/com/iemr/mmu/repo/nikshay/NikshayBeneficiarySyncStatusRepo.java @@ -36,7 +36,7 @@ public interface NikshayBeneficiarySyncStatusRepo extends JpaRepository Date: Tue, 3 Nov 2026 22:13:07 +0530 Subject: [PATCH 4/7] changes for the bulk device upload url --- src/main/environment/common_ci.properties | 2 +- src/main/environment/common_docker.properties | 2 +- .../nikshay/impl/NikshaySyncStatusServiceImpl.java | 9 ++++----- src/main/resources/application.properties | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/main/environment/common_ci.properties b/src/main/environment/common_ci.properties index ffcafe41..4462375e 100644 --- a/src/main/environment/common_ci.properties +++ b/src/main/environment/common_ci.properties @@ -98,4 +98,4 @@ springdoc.swagger-ui.enabled=@env.SWAGGER_DOC_ENABLED@ cors.allowed-origins=@env.CORS_ALLOWED_ORIGINS@ # Server that hosts the failedBeneficiaries API -nikshay.baseUrl=@env.NIKSHAY_BASE_URL@ +nikshay.failedBeneficiariesUrl=@env.MMU_CENTRAL_SERVER@nikshay/syncStatus/failedBeneficiaries diff --git a/src/main/environment/common_docker.properties b/src/main/environment/common_docker.properties index 23a68d0e..11a36cd6 100644 --- a/src/main/environment/common_docker.properties +++ b/src/main/environment/common_docker.properties @@ -96,4 +96,4 @@ springdoc.api-docs.enabled=${SWAGGER_DOC_ENABLED} springdoc.swagger-ui.enabled=${SWAGGER_DOC_ENABLED} # Server that hosts the failedBeneficiaries API -nikshay.baseUrl=${NIKSHAY_BASE_URL} +nikshay.failedBeneficiariesUrl=${MMU_CENTRAL_SERVER}nikshay/syncStatus/failedBeneficiaries diff --git a/src/main/java/com/iemr/mmu/service/nikshay/impl/NikshaySyncStatusServiceImpl.java b/src/main/java/com/iemr/mmu/service/nikshay/impl/NikshaySyncStatusServiceImpl.java index 810de49d..72389723 100644 --- a/src/main/java/com/iemr/mmu/service/nikshay/impl/NikshaySyncStatusServiceImpl.java +++ b/src/main/java/com/iemr/mmu/service/nikshay/impl/NikshaySyncStatusServiceImpl.java @@ -9,7 +9,6 @@ import com.iemr.mmu.repo.nikshay.NikshayBeneficiaryPhaseStatusRepo; import com.iemr.mmu.repo.nikshay.NikshayBeneficiarySyncStatusRepo; import com.iemr.mmu.service.nikshay.NikshaySyncStatusService; -import com.iemr.mmu.utils.NikshayConstants; import jakarta.annotation.PostConstruct; import org.json.JSONArray; import org.json.JSONObject; @@ -64,9 +63,9 @@ public class NikshaySyncStatusServiceImpl implements NikshaySyncStatusService { @Autowired private NikshayBeneficiarySyncStatusRepo syncStatusRepo; - // Server that hosts the failedBeneficiaries API (it reads nikshay_beneficiary_phase_status) - @Value("${nikshay.baseUrl}") - private String nikshayBaseUrl; + // Full URL of the failedBeneficiaries API (it reads nikshay_beneficiary_phase_status) + @Value("${nikshay.failedBeneficiariesUrl}") + private String failedBeneficiariesUrl; @Value("${nikshay.http.connectTimeoutMs:15000}") private int connectTimeoutMs; @@ -293,7 +292,7 @@ private List fetchFailedPhases(String serverAuthor ResponseEntity response; try { - String url = nikshayBaseUrl + NikshayConstants.NIKSHAY_PATH + NikshayConstants.FAILED_BENEFICIARIES_PATH; + String url = failedBeneficiariesUrl; response = restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(headers), String.class); } catch (HttpStatusCodeException e) { diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 92fe6fc3..4a6b241e 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -64,4 +64,4 @@ server.undertow.max-http-post-size=-1 # ============= NIKSHAY ============= # Server that hosts the failedBeneficiaries API -#nikshay.baseUrl=http://10.208.122.38:8080/mmu-api +#nikshay.failedBeneficiariesUrl=http://10.208.122.38:8080/mmu-api/nikshay/syncStatus/failedBeneficiaries From aa7bfa1efebb9378d0919485d08f8fe8904d65c0 Mon Sep 17 00:00:00 2001 From: Pankaj Kumar Date: Wed, 4 Nov 2026 10:54:33 +0530 Subject: [PATCH 5/7] changes for PR comment deleted unused files --- .../environment/common_example.properties | 2 + .../mmu/data/nikshay/Beneficiarydetail.java | 231 ------------------ .../mmu/repo/nikshay/BeneficiaryRepo.java | 20 -- .../NikshayBeneficiaryPhaseStatusRepo.java | 2 - 4 files changed, 2 insertions(+), 253 deletions(-) delete mode 100644 src/main/java/com/iemr/mmu/data/nikshay/Beneficiarydetail.java delete mode 100644 src/main/java/com/iemr/mmu/repo/nikshay/BeneficiaryRepo.java diff --git a/src/main/environment/common_example.properties b/src/main/environment/common_example.properties index 294c0e45..06856ccb 100644 --- a/src/main/environment/common_example.properties +++ b/src/main/environment/common_example.properties @@ -80,6 +80,8 @@ tmCentralServer = /tm-api/common/get/Case-sheet/ specialistSign = /common-api/signature1/getSignClass +nikshay.failedBeneficiariesUrl=http://10.208.122.38:8080/mmu-api/nikshay/syncStatus/failedBeneficiaries + fileBasePath = ## data sync batch size diff --git a/src/main/java/com/iemr/mmu/data/nikshay/Beneficiarydetail.java b/src/main/java/com/iemr/mmu/data/nikshay/Beneficiarydetail.java deleted file mode 100644 index 2aded129..00000000 --- a/src/main/java/com/iemr/mmu/data/nikshay/Beneficiarydetail.java +++ /dev/null @@ -1,231 +0,0 @@ -package com.iemr.mmu.data.nikshay; - -import com.google.gson.annotations.Expose; -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 jakarta.persistence.Transient; -import lombok.Data; - -import java.math.BigInteger; -import java.sql.Timestamp; - -@Entity -@Table(name = "i_beneficiarydetails", schema = "db_identity") -@Data -public class Beneficiarydetail { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(unique = true, nullable = false) - private BigInteger beneficiaryDetailsId; - - private Integer areaId; - - @Column(name = "beneficiaryRegID") - private BigInteger BenRegId; - - @Column(length = 45) - private String community; - - @Column(nullable = false, length = 50, updatable = false) - private String createdBy; - - @Column(name = "CreatedDate") - private Timestamp createdDate; - - @Column(name = "dob") - private Timestamp dob; - - private Boolean deleted = false; - - @Column(length = 45) - private String education; - - private Boolean emergencyRegistration; - - @Column(length = 50) - private String fatherName; - - @Column(length = 20) - private String firstName; - - @Column(length = 50) - private String motherName; - - private String gender; - - @Column(name = "incomeStatusId") - private Integer incomeStatusId; - - @Column(length = 45) - private String incomeStatus; - - private Timestamp lastModDate; - - @Column(length = 20) - private String lastName; - - @Column(length = 20) - private String maritalstatus; - - @Column(name = "marriageDate") - private Timestamp marriageDate; - - @Transient - private Integer ageAtMarriage; - - @Column(name = "MiddleName", length = 20) - private String middleName; - - @Column(length = 50) - private String modifiedBy; - - private Integer occupationId; - - @Column(length = 45) - private String occupation; - - private Integer phcId; - - @Column(length = 30) - private String placeOfWork; - - private String literacyStatus; - - private Integer preferredLanguageId; - - @Column(length = 45) - private String preferredLanguage; - - @Column(nullable = false, length = 4) - private String processed = "N"; - - @Column(length = 45) - private BigInteger religionID; - - @Column(length = 45) - private String religion; - - @Column(length = 300) - private String remarks; - - private Boolean reserved; - - private Integer reservedById; - - @Column(length = 45) - private String reservedFor; - - private Timestamp reservedOn; - - private BigInteger servicePointId; - - @Column(length = 45) - private String sourceOfInfo; - - @Column(length = 50) - private String spousename; - - @Column(length = 10) - private String status; - - @Column(length = 10) - private String title; - - private Integer zoneId; - - @Column(name = "GenderID") - private Integer genderId; - - @Column(name = "MaritalStatusID") - private Integer maritalstatusId; - - @Column(name = "TitleID") - private Integer titleId; - - @Column(name = "CommunityID") - private Integer communityId; - - @Column(name = "EducationID") - private Integer educationId; - - @Column(name = "SexualOrientationID") - private Integer sexualOrientationID; - - @Column(name = "SexualOrientationType") - private String sexualOrientationType; - - @Column(name = "HIVStatus") - private Integer isHIVPositive; - - @Column(name = "HealthCareWorkerId") - private Integer healthCareWorkerId; - - private String healthCareWorker; - - @Expose - @Column(name = "vanID", updatable = false) - private Integer VanID; - @Expose - @Column(name = "parkingPlaceID", updatable = false) - private Integer parkingPlaceID; - - @Expose - @Column(name = "VanSerialNo", updatable = false) - private BigInteger id; - - @Expose - @Column(name = "SyncedBy") - private String SyncedBy; - - @Expose - @Column(name = "SyncedDate") - private Timestamp SyncedDate; - - @Transient - private BigInteger benficieryid; - - @Expose - @Transient - private Integer ProviderServiceMapID; - - @Expose - @Column(name = "familyid") - private String familyId; - - @Column(name = "ExtraFields") - private String otherFields; - - @Expose - @Column(name = "economicStatus") - private String economicStatus; - - @Expose - @Column(name = "economicStatusId") - private Integer economicStatusId; - - @Expose - @Column(name = "residentialArea") - private String residentialArea; - - @Expose - @Column(name = "residentialAreaId") - private Integer residentialAreaId; - - @Expose - @Column(name = "address") - private String address; - - @Expose - private String placeOfCurrentLiving; - - @Expose - private String otherPlaceOfCurrentLiving; - - @Expose - private String institutionName; - -} diff --git a/src/main/java/com/iemr/mmu/repo/nikshay/BeneficiaryRepo.java b/src/main/java/com/iemr/mmu/repo/nikshay/BeneficiaryRepo.java deleted file mode 100644 index 24bf80d8..00000000 --- a/src/main/java/com/iemr/mmu/repo/nikshay/BeneficiaryRepo.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.iemr.mmu.repo.nikshay; - - -import com.iemr.mmu.data.nikshay.Beneficiarydetail; -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.math.BigInteger; -import java.util.Collection; -import java.util.List; - -@Repository -public interface BeneficiaryRepo extends JpaRepository { - - @Query("SELECT d FROM Beneficiarydetail d WHERE d.BenRegId IN :benRegIDs") - List findDetailsByBenRegIDIn(@Param("benRegIDs") Collection benRegIDs); - -} diff --git a/src/main/java/com/iemr/mmu/repo/nikshay/NikshayBeneficiaryPhaseStatusRepo.java b/src/main/java/com/iemr/mmu/repo/nikshay/NikshayBeneficiaryPhaseStatusRepo.java index f2689842..852f6a81 100644 --- a/src/main/java/com/iemr/mmu/repo/nikshay/NikshayBeneficiaryPhaseStatusRepo.java +++ b/src/main/java/com/iemr/mmu/repo/nikshay/NikshayBeneficiaryPhaseStatusRepo.java @@ -12,8 +12,6 @@ @Repository public interface NikshayBeneficiaryPhaseStatusRepo extends JpaRepository { - List findByBenRegIDIn(Collection benRegIDs); - // Every phase row (any status) of every beneficiary that has at least one phase in the given status, // newest beneficiary first @Query("SELECT p FROM NikshayBeneficiaryPhaseStatus p WHERE p.benRegID IN " + From eb0da96c81daa43a3741527ff5672ebf58cc24c4 Mon Sep 17 00:00:00 2001 From: Pankaj Kumar Date: Wed, 4 Nov 2026 11:18:06 +0530 Subject: [PATCH 6/7] collumn name changed --- .../com/iemr/mmu/data/nikshay/NikshayBeneficiarySyncStatus.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/iemr/mmu/data/nikshay/NikshayBeneficiarySyncStatus.java b/src/main/java/com/iemr/mmu/data/nikshay/NikshayBeneficiarySyncStatus.java index ff72cd23..cb27934f 100644 --- a/src/main/java/com/iemr/mmu/data/nikshay/NikshayBeneficiarySyncStatus.java +++ b/src/main/java/com/iemr/mmu/data/nikshay/NikshayBeneficiarySyncStatus.java @@ -59,7 +59,7 @@ public class NikshayBeneficiarySyncStatus { @Column(name = "last_attempt_at") private Timestamp lastAttemptAt; - @Column(name = "isUpdated", nullable = false) + @Column(name = "is_updated", nullable = false) private Boolean isUpdated = false; @CreationTimestamp From 084d0cae268d375ef38ed34a71f163203019b904 Mon Sep 17 00:00:00 2001 From: Pankaj Kumar Date: Wed, 4 Nov 2026 11:49:52 +0530 Subject: [PATCH 7/7] cohnanged in properties file --- src/main/environment/common_example.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/environment/common_example.properties b/src/main/environment/common_example.properties index 06856ccb..92911067 100644 --- a/src/main/environment/common_example.properties +++ b/src/main/environment/common_example.properties @@ -80,7 +80,7 @@ tmCentralServer = /tm-api/common/get/Case-sheet/ specialistSign = /common-api/signature1/getSignClass -nikshay.failedBeneficiariesUrl=http://10.208.122.38:8080/mmu-api/nikshay/syncStatus/failedBeneficiaries +nikshay.failedBeneficiariesUrl=http://localhost:8087/mmu-api/nikshay/syncStatus/failedBeneficiaries fileBasePath =