diff --git a/pom.xml b/pom.xml index 04fad9b0..667d7cc6 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.iemr.common.identity identity-api - 3.8.3 + 3.12.0 war diff --git a/src/main/java/com/iemr/common/identity/controller/IdentityController.java b/src/main/java/com/iemr/common/identity/controller/IdentityController.java index 2482ac47..6468ed3e 100644 --- a/src/main/java/com/iemr/common/identity/controller/IdentityController.java +++ b/src/main/java/com/iemr/common/identity/controller/IdentityController.java @@ -990,6 +990,8 @@ public String saveGeneratedBenIDToLocalServer( logger.info("Number of unique benid imported to local server: " + i); if (i > 0) response.setResponse(i + " Unique benid imported to local server"); + else if (!benIdImportDTOList.isEmpty()) + response.setResponse("All benid already present in local server"); else { response.setResponse("Empty or invalid data"); logger.error("Empty or invalid data. Data Size is : " + benIdImportDTOList.size()); diff --git a/src/main/java/com/iemr/common/identity/data/rmnch/BenAnthropometryDetail.java b/src/main/java/com/iemr/common/identity/data/rmnch/BenAnthropometryDetail.java new file mode 100644 index 00000000..27a6df6e --- /dev/null +++ b/src/main/java/com/iemr/common/identity/data/rmnch/BenAnthropometryDetail.java @@ -0,0 +1,101 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.common.identity.data.rmnch; + +import java.sql.Timestamp; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import lombok.Data; + +/** + * Mirrors FLW-API's BenAnthropometryDetail (same db_iemr.t_phy_anthropometry table), so syncDataToAmrit can write + * registration-time values that FLW-API getBeneficiaryData reads back. + */ +@Entity +@Table(name = "t_phy_anthropometry", catalog = "db_iemr") +@Data +public class BenAnthropometryDetail { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "ID") + private Long id; + + @Column(name = "BeneficiaryRegID") + private Long beneficiaryRegID; + + @Column(name = "BenVisitID") + private Long benVisitID; + + @Column(name = "ProviderServiceMapID") + private Integer providerServiceMapID; + + @Column(name = "VisitCode") + private Long visitCode; + + @Column(name = "Weight_Kg") + private Double weightKg; + + @Column(name = "Height_cm") + private Double heightCm; + + @Column(name = "BMI") + private Double bmi; + + @Column(name = "Deleted", insertable = false, updatable = true) + private Boolean deleted; + + @Column(name = "Processed", insertable = false, updatable = true) + private String processed; + + @Column(name = "CreatedBy") + private String createdBy; + + @Column(name = "CreatedDate", insertable = false, updatable = false) + private Timestamp createdDate; + + @Column(name = "ModifiedBy") + private String modifiedBy; + + @Column(name = "LastModDate", insertable = false, updatable = false) + private Timestamp lastModDate; + + @Column(name = "VanSerialNo") + private Long vanSerialNo; + + @Column(name = "VanID") + private Integer vanID; + + @Column(name = "ParkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "SyncedBy") + private String syncedBy; + + @Column(name = "SyncedDate") + private Timestamp syncedDate; +} diff --git a/src/main/java/com/iemr/common/identity/data/rmnch/BenPhysicalVitalDetail.java b/src/main/java/com/iemr/common/identity/data/rmnch/BenPhysicalVitalDetail.java new file mode 100644 index 00000000..9e98f289 --- /dev/null +++ b/src/main/java/com/iemr/common/identity/data/rmnch/BenPhysicalVitalDetail.java @@ -0,0 +1,107 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.common.identity.data.rmnch; + +import java.sql.Timestamp; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import lombok.Data; + +/** + * Mirrors FLW-API's BenPhysicalVitalDetail (same db_iemr.t_phy_vitals table), so syncDataToAmrit can write + * registration-time values that FLW-API getBeneficiaryData reads back. + */ +@Entity +@Table(name = "t_phy_vitals", catalog = "db_iemr") +@Data +public class BenPhysicalVitalDetail { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "ID") + private Long id; + + @Column(name = "BeneficiaryRegID") + private Long beneficiaryRegID; + + @Column(name = "BenVisitID") + private Long benVisitID; + + @Column(name = "ProviderServiceMapID") + private Integer providerServiceMapID; + + @Column(name = "VisitCode") + private Long visitCode; + + @Column(name = "Temperature") + private Double temperature; + + @Column(name = "PulseRate") + private Short pulseRate; + + @Column(name = "SystolicBP_1stReading") + private Short systolicBP; + + @Column(name = "DiastolicBP_1stReading") + private Short diastolicBP; + + @Column(name = "BloodGlucose_Random") + private Short bloodGlucoseRandom; + + @Column(name = "Deleted", insertable = false, updatable = true) + private Boolean deleted; + + @Column(name = "Processed", insertable = false, updatable = true) + private String processed; + + @Column(name = "CreatedBy") + private String createdBy; + + @Column(name = "CreatedDate", insertable = false, updatable = false) + private Timestamp createdDate; + + @Column(name = "ModifiedBy") + private String modifiedBy; + + @Column(name = "LastModDate", insertable = false, updatable = false) + private Timestamp lastModDate; + + @Column(name = "VanSerialNo") + private Long vanSerialNo; + + @Column(name = "VanID") + private Integer vanID; + + @Column(name = "ParkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "SyncedBy") + private String syncedBy; + + @Column(name = "SyncedDate") + private Timestamp syncedDate; +} diff --git a/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHBeneficiaryDetailsRmnch.java b/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHBeneficiaryDetailsRmnch.java index 06022622..2d80fc8f 100644 --- a/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHBeneficiaryDetailsRmnch.java +++ b/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHBeneficiaryDetailsRmnch.java @@ -611,12 +611,14 @@ public class RMNCHBeneficiaryDetailsRmnch { @Transient private Double temperature; // stored as "temperatureValue" in otherFields to match getBeneficiaryData key - @Expose @Column(name = "digipin") private String digipin; - + @Expose + @Column(name = "gpsTimestamp") + @JsonAdapter(GpsTimestampAdapter.class) + private Timestamp gpsTimestamp; @Expose @Column(name = "isGpsUnavailable", nullable = false, columnDefinition = "TINYINT(1) DEFAULT 0") diff --git a/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHHouseHoldDetails.java b/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHHouseHoldDetails.java index 1e794702..f5233bbf 100644 --- a/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHHouseHoldDetails.java +++ b/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHHouseHoldDetails.java @@ -422,4 +422,31 @@ public class RMNCHHouseHoldDetails { @Column(name = "gpsUnavailableReason") private String gpsUnavailableReason; + @Expose + @SerializedName("latitude") + @Column(name = "gpsLatitude") + private Double gpsLatitude; + + @Expose + @SerializedName("longitude") + @Column(name = "gpsLongitude") + private Double gpsLongitude; + + @Expose + @Column(name = "digipin") + private String digipin; + + @Expose + @Column(name = "gpsTimestamp") + @JsonAdapter(GpsTimestampAdapter.class) + private Timestamp gpsTimestamp; + + @Expose + @Column(name = "isGpsUnavailable", nullable = false, columnDefinition = "TINYINT(1) DEFAULT 0") + private Boolean isGpsUnavailable = false; + + @Expose + @Column(name = "gpsUnavailableReason") + private String gpsUnavailableReason; + } diff --git a/src/main/java/com/iemr/common/identity/domain/Address.java b/src/main/java/com/iemr/common/identity/domain/Address.java index 7f7e716b..f355cbc8 100644 --- a/src/main/java/com/iemr/common/identity/domain/Address.java +++ b/src/main/java/com/iemr/common/identity/domain/Address.java @@ -56,9 +56,11 @@ private Integer vanID; private Integer parkingPlaceID; - + private Double gpsLatitude; + private Double gpsLongitude; private String digipin; - + @JsonAdapter(GpsTimestampAdapter.class) + private Timestamp gpsTimestamp; private Boolean isGpsUnavailable; private String gpsUnavailableReason; } diff --git a/src/main/java/com/iemr/common/identity/repo/BenRegIdMappingRepo.java b/src/main/java/com/iemr/common/identity/repo/BenRegIdMappingRepo.java index 866b2137..ea629982 100644 --- a/src/main/java/com/iemr/common/identity/repo/BenRegIdMappingRepo.java +++ b/src/main/java/com/iemr/common/identity/repo/BenRegIdMappingRepo.java @@ -60,6 +60,12 @@ MBeneficiaryregidmapping findFirstByProviderServiceMapIDAndVehicalNoOrderByBenRe Long countByProvisioned(Boolean isProvisioned); + /** + * Count of BenRegIds that can be given to a new registration: not provisioned + * and not claimed by a registration in progress. + */ + Long countByProvisionedAndReserved(Boolean isProvisioned, Boolean isReserved); + @Transactional @Modifying @Query("update MBeneficiaryregidmapping set providerServiceMapID = null , vehicalNo = null " diff --git a/src/main/java/com/iemr/common/identity/repo/rmnch/BenAnthropometryRepo.java b/src/main/java/com/iemr/common/identity/repo/rmnch/BenAnthropometryRepo.java new file mode 100644 index 00000000..95476f67 --- /dev/null +++ b/src/main/java/com/iemr/common/identity/repo/rmnch/BenAnthropometryRepo.java @@ -0,0 +1,41 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.common.identity.repo.rmnch; + +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import com.iemr.common.identity.data.rmnch.BenAnthropometryDetail; + +@Repository +public interface BenAnthropometryRepo extends CrudRepository { + public BenAnthropometryDetail findFirstByBeneficiaryRegIDOrderByCreatedDateDescIdDesc(Long beneficiaryRegID); + + @Transactional + @Modifying + @Query("UPDATE BenAnthropometryDetail t SET t.vanSerialNo = t.id WHERE t.id = :id") + void updateVanSerialNo(@Param("id") Long id); +} diff --git a/src/main/java/com/iemr/common/identity/repo/rmnch/BenPhysicalVitalRepo.java b/src/main/java/com/iemr/common/identity/repo/rmnch/BenPhysicalVitalRepo.java new file mode 100644 index 00000000..61ddc045 --- /dev/null +++ b/src/main/java/com/iemr/common/identity/repo/rmnch/BenPhysicalVitalRepo.java @@ -0,0 +1,41 @@ +/* +* AMRIT – Accessible Medical Records via Integrated Technology +* Integrated EHR (Electronic Health Records) Solution +* +* Copyright (C) "Piramal Swasthya Management and Research Institute" +* +* This file is part of AMRIT. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ +package com.iemr.common.identity.repo.rmnch; + +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import com.iemr.common.identity.data.rmnch.BenPhysicalVitalDetail; + +@Repository +public interface BenPhysicalVitalRepo extends CrudRepository { + public BenPhysicalVitalDetail findFirstByBeneficiaryRegIDAndTemperatureIsNotNullOrderByCreatedDateDescIdDesc(Long beneficiaryRegID); + + @Transactional + @Modifying + @Query("UPDATE BenPhysicalVitalDetail t SET t.vanSerialNo = t.id WHERE t.id = :id") + void updateVanSerialNo(@Param("id") Long id); +} diff --git a/src/main/java/com/iemr/common/identity/service/IdentityService.java b/src/main/java/com/iemr/common/identity/service/IdentityService.java index 0d965dd5..f1872cb1 100644 --- a/src/main/java/com/iemr/common/identity/service/IdentityService.java +++ b/src/main/java/com/iemr/common/identity/service/IdentityService.java @@ -2170,7 +2170,9 @@ public int importBenIdToLocalServer(List benIdImportDTOList) { jdbcTemplate = getJdbcTemplate(); List dataList = new ArrayList<>(); Object[] objArr; - String query = " INSERT INTO m_beneficiaryregidmapping(BenRegId, BeneficiaryID, " + // INSERT IGNORE: importing the same IDs again (retry after a failed or partial + // import) skips rows already present instead of failing the whole batch + String query = " INSERT IGNORE INTO m_beneficiaryregidmapping(BenRegId, BeneficiaryID, " + " Provisioned, CreatedDate, CreatedBy, Reserved, vanID) VALUES (?,?,?,?,?,?,?) "; logger.info("query : " + query); for (MBeneficiaryregidmapping obj : mBeneficiaryregidmappingList) { @@ -2191,9 +2193,27 @@ public int importBenIdToLocalServer(List benIdImportDTOList) { logger.info("regid :" + obj.getBenRegId() + " - benid :" + obj.getBeneficiaryID()); } + // batched inserts do not report per-row results, so count the rows in this + // BenRegId range before and after to know how many were really added + java.math.BigInteger minId = null; + java.math.BigInteger maxId = null; + for (MBeneficiaryregidmapping obj : mBeneficiaryregidmappingList) { + java.math.BigInteger id = obj.getBenRegId(); + if (id != null) { + minId = (minId == null || id.compareTo(minId) < 0) ? id : minId; + maxId = (maxId == null || id.compareTo(maxId) > 0) ? id : maxId; + } + } + String rangeCount = "SELECT COUNT(*) FROM m_beneficiaryregidmapping WHERE BenRegId BETWEEN ? AND ?"; + Long before = minId == null ? 0L : jdbcTemplate.queryForObject(rangeCount, Long.class, minId, maxId); + int[] i = jdbcTemplate.batchUpdate(query, dataList); - return i.length; + Long after = minId == null ? 0L : jdbcTemplate.queryForObject(rangeCount, Long.class, minId, maxId); + int inserted = (int) (after - before); + logger.info("IdentityService.importBenIdToLocalServer - received " + i.length + ", newly inserted " + + inserted + ", already present or skipped " + (i.length - inserted)); + return inserted; } else { return 0; } @@ -2201,7 +2221,9 @@ public int importBenIdToLocalServer(List benIdImportDTOList) { } public Long checkBenIDAvailabilityLocal() { - return regIdRepo.countByProvisioned(false); + // Provisioned=false alone also counts rows claimed (Reserved=true) by a registration + // that never completed; those cannot be given to a new registration + return regIdRepo.countByProvisionedAndReserved(false, false); } diff --git a/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java b/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java index 715458e0..e4062bb3 100644 --- a/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java +++ b/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java @@ -51,7 +51,9 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.iemr.common.identity.controller.rmnch.RMNCHMobileAppController; +import com.iemr.common.identity.data.rmnch.BenAnthropometryDetail; import com.iemr.common.identity.data.rmnch.BenHealthIDDetails; +import com.iemr.common.identity.data.rmnch.BenPhysicalVitalDetail; import com.iemr.common.identity.data.rmnch.GetBenRequestHandler; import com.iemr.common.identity.data.rmnch.NcdTbHrpData; import com.iemr.common.identity.data.rmnch.RMNCHBeneficiaryDetailsRmnch; @@ -64,6 +66,8 @@ import com.iemr.common.identity.data.rmnch.RMNCHMBeneficiarycontact; import com.iemr.common.identity.data.rmnch.RMNCHMBeneficiarydetail; import com.iemr.common.identity.data.rmnch.RMNCHMBeneficiarymapping; +import com.iemr.common.identity.repo.rmnch.BenAnthropometryRepo; +import com.iemr.common.identity.repo.rmnch.BenPhysicalVitalRepo; import com.iemr.common.identity.repo.rmnch.RMNCHBenAccountRepo; import com.iemr.common.identity.repo.rmnch.RMNCHBenAddressRepo; import com.iemr.common.identity.repo.rmnch.RMNCHBenContactRepo; @@ -118,6 +122,10 @@ public class RmnchDataSyncServiceImpl implements RmnchDataSyncService { private RMNCHMBenRegIdMapRepo rMNCHMBenRegIdMapRepo; @Autowired private BenDetailRepo benDetailRepo; + @Autowired + private BenAnthropometryRepo benAnthropometryRepo; + @Autowired + private BenPhysicalVitalRepo benPhysicalVitalRepo; @Value("${fhir-url}") private String fhirUrl; @@ -212,10 +220,14 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex // Extract GPS from i_bendemographics JsonObject demog = benGpsMap.get(obj.getBenficieryid()); if (demog != null) { - + if (demog.has("latitude") && !demog.get("latitude").isJsonNull()) + obj.setLatitude(demog.get("latitude").getAsBigDecimal()); + if (demog.has("longitude") && !demog.get("longitude").isJsonNull()) + obj.setLongitude(demog.get("longitude").getAsBigDecimal()); if (demog.has("digipin") && !demog.get("digipin").isJsonNull()) obj.setDigipin(demog.get("digipin").getAsString()); - + if (demog.has("gpsTimestamp") && !demog.get("gpsTimestamp").isJsonNull()) + obj.setGpsTimestamp(new Timestamp(demog.get("gpsTimestamp").getAsLong())); if (demog.has("isGpsUnavailable") && !demog.get("isGpsUnavailable").isJsonNull()) obj.setIsGpsUnavailable(demog.get("isGpsUnavailable").getAsBoolean()); // STOP-444 followup fix: same source object, previously never read for @@ -327,6 +339,7 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex rmnchmBeneficiarydetail.setMaritalstatusId(obj.getMaritalstatusId()); rmnchmBeneficiarydetail.setPlaceOfCurrentLiving(obj.getPlaceOfCurrentLiving()); rmnchmBeneficiarydetail.setOtherPlaceOfCurrentLiving(obj.getOtherPlaceOfCurrentLiving()); + rmnchmBeneficiarydetail.setInstitutionName(obj.getInstitutionName()); // STOP-444 followup fix: these were previously never re-synced on an edit of an // existing beneficiary, so any change to them from the app was silently dropped. if (obj.getCommunity() != null) { @@ -404,19 +417,13 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex // update beneficiary data in i_beneficiarydetails table rMNCHBenDetailsRepo.saveAll(benDetailsList); - // Write anthropometry (height/weight/bmi/temperature) to i_beneficiarydetails.otherFields. - // i_beneficiarydetails_rmnch has no these columns; FLW-API getBeneficiaryData reads from otherFields. + // Write anthropometry (height/weight/bmi/temperature) to db_iemr.t_phy_anthropometry / + // t_phy_vitals — i_beneficiarydetails_rmnch has no these columns. FLW-API getBeneficiaryData + // and the nurse exam save read them from there. for (RMNCHBeneficiaryDetailsRmnch obj : benDetailsOriginalList) { if (obj.getBenRegId() != null && hasAnthropometryData(obj)) { - try { - MBeneficiarydetail benDetail = benDetailRepo.findByBenRegId(obj.getBenRegId()); - if (benDetail != null) { - String merged = mergeAnthropometry(benDetail.getOtherFields(), obj); - benDetailRepo.updateOtherFieldsByBenRegId(obj.getBenRegId(), merged); - } - } catch (Exception ex) { - logger.warn("Failed to update otherFields for benRegId: " + obj.getBenRegId() + " - " + ex.getMessage()); - } + writeAnthropometry(obj); + writeVitals(obj); } } @@ -501,7 +508,8 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex .getByHouseHoldID(obj.getHouseoldId()).get(0); if (temp != null) obj.setHouseHoldDetailsId(temp.getHouseHoldDetailsId()); - + if (hhTimestampMap.containsKey(obj.getHouseoldId())) + obj.setGpsTimestamp(new Timestamp(hhTimestampMap.get(obj.getHouseoldId()))); } // Set VanID/ParkingPlaceID for both NEW and existing households — this must // stay OUTSIDE the "household already exists" block above (it's regressed @@ -875,20 +883,95 @@ private boolean hasAnthropometryData(RMNCHBeneficiaryDetailsRmnch obj) { || obj.getBmi() != null || obj.getTemperature() != null; } - private String mergeAnthropometry(String existingOtherFields, RMNCHBeneficiaryDetailsRmnch obj) { - JsonObject json = new JsonObject(); - if (existingOtherFields != null && !existingOtherFields.isBlank()) { - try { - json = new JsonParser().parse(existingOtherFields).getAsJsonObject(); - } catch (Exception ignored) { - } + // Inserts a new t_phy_anthropometry row only when height/weight/bmi changed vs the latest row, + // carrying forward any value the app didn't send — from the latest row, or from otherFields for + // beneficiaries whose values were stored there before this table was written. Not tied to a visit — + // BenVisitID/VisitCode are nullable, and FLW-API reads latest by BeneficiaryRegID. + // Not wrapped in try/catch: these tables are now the only store for these values, and a failed save + // marks the sync transaction rollback-only anyway, so let the sync fail and the app retry. + private void writeAnthropometry(RMNCHBeneficiaryDetailsRmnch obj) { + if (obj.getHeight() == null && obj.getWeight() == null && obj.getBmi() == null) + return; + Long benRegID = obj.getBenRegId().longValue(); + if (obj.getCreatedBy() == null) { + logger.warn("Skipping t_phy_anthropometry write, createdBy missing for benRegId: " + benRegID); + return; + } + BenAnthropometryDetail latest = benAnthropometryRepo + .findFirstByBeneficiaryRegIDOrderByCreatedDateDescIdDesc(benRegID); + JsonObject legacy = latest == null ? getLegacyAnthropometry(obj.getBenRegId()) : new JsonObject(); + Double height = obj.getHeight() != null ? obj.getHeight() + : (latest != null ? latest.getHeightCm() : getDouble(legacy, "height")); + Double weight = obj.getWeight() != null ? obj.getWeight() + : (latest != null ? latest.getWeightKg() : getDouble(legacy, "weight")); + Double bmi = obj.getBmi() != null ? obj.getBmi() : (latest != null ? latest.getBmi() : getDouble(legacy, "bmi")); + if (latest != null && sameValue(latest.getHeightCm(), height) && sameValue(latest.getWeightKg(), weight) + && sameValue(latest.getBmi(), bmi)) + return; + + BenAnthropometryDetail a = new BenAnthropometryDetail(); + a.setBeneficiaryRegID(benRegID); + a.setProviderServiceMapID(obj.getProviderServiceMapID()); + a.setCreatedBy(obj.getCreatedBy()); + a.setVanID(obj.getVanID()); + a.setParkingPlaceID(obj.getParkingPlaceID()); + a.setHeightCm(height); + a.setWeightKg(weight); + a.setBmi(bmi); + a = benAnthropometryRepo.save(a); + benAnthropometryRepo.updateVanSerialNo(a.getId()); + } + + // Inserts a new t_phy_vitals row only when temperature changed vs the latest recorded temperature. + // Pulse/BP/glucose aren't part of registration, so they stay null here. + private void writeVitals(RMNCHBeneficiaryDetailsRmnch obj) { + if (obj.getTemperature() == null) + return; + Long benRegID = obj.getBenRegId().longValue(); + if (obj.getCreatedBy() == null) { + logger.warn("Skipping t_phy_vitals write, createdBy missing for benRegId: " + benRegID); + return; + } + BenPhysicalVitalDetail latest = benPhysicalVitalRepo + .findFirstByBeneficiaryRegIDAndTemperatureIsNotNullOrderByCreatedDateDescIdDesc(benRegID); + if (latest != null && sameValue(latest.getTemperature(), obj.getTemperature())) + return; + + BenPhysicalVitalDetail v = new BenPhysicalVitalDetail(); + v.setBeneficiaryRegID(benRegID); + v.setProviderServiceMapID(obj.getProviderServiceMapID()); + v.setCreatedBy(obj.getCreatedBy()); + v.setVanID(obj.getVanID()); + v.setParkingPlaceID(obj.getParkingPlaceID()); + v.setTemperature(obj.getTemperature()); + v = benPhysicalVitalRepo.save(v); + benPhysicalVitalRepo.updateVanSerialNo(v.getId()); + } + + // Columns are DECIMAL(5,2), so compare at that precision. + private boolean sameValue(Double stored, Double incoming) { + if (stored == null || incoming == null) + return stored == incoming; + return Math.round(stored * 100) == Math.round(incoming * 100); + } + + private JsonObject getLegacyAnthropometry(BigInteger benRegID) { + try { + MBeneficiarydetail benDetail = benDetailRepo.findByBenRegId(benRegID); + if (benDetail != null && benDetail.getOtherFields() != null && !benDetail.getOtherFields().isBlank()) + return new JsonParser().parse(benDetail.getOtherFields()).getAsJsonObject(); + } catch (Exception ex) { + logger.warn("Cannot read otherFields for benRegId: " + benRegID + " - " + ex.getMessage()); + } + return new JsonObject(); + } + + private Double getDouble(JsonObject json, String key) { + try { + return json.has(key) && !json.get(key).isJsonNull() ? json.get(key).getAsDouble() : null; + } catch (Exception ex) { + return null; } - if (obj.getHeight() != null) json.addProperty("height", obj.getHeight()); - if (obj.getWeight() != null) json.addProperty("weight", obj.getWeight()); - if (obj.getBmi() != null) json.addProperty("bmi", obj.getBmi()); - // mobile sends "temperature"; FLW-API getBeneficiaryData reads "temperatureValue" - if (obj.getTemperature() != null) json.addProperty("temperatureValue", obj.getTemperature()); - return new Gson().toJson(json); } @Override