From 485ae8ff2c3a840fd6c48a52a6321155c135ae75 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Fri, 11 Sep 2026 12:11:55 +0530 Subject: [PATCH 1/3] fix: sync beneficiary occupation/community/religion/pincode/phone on edit (STOP-444) RmnchDataSyncServiceImpl's edit-sync path (syncDataToAmrit) only ever wrote a fixed 13-field whitelist to i_beneficiarydetails when updating an existing beneficiary (name/dob/gender/maritalstatus/family fields). occupation, community, religion, pincode, and phone number were silently dropped on every edit, regardless of what the app actually sent. Root cause: these fields are either sent nested inside i_bendemographics (only ever read for GPS before this fix), in a benPhoneMaps[] array, or under lowercase JSON keys (e.g. "villageid") that don't case-match the Java field names (e.g. villageId) under Gson's default case-sensitive binding. Traced and verified against real captured edit payloads, not assumed field names. Changes: - RMNCHBeneficiaryDetailsRmnch: add occupation/occupationId/phoneNo fields (previously had no field to bind these into at all). - RMNCHMBenMappingRepo: add getByBenRegId(), needed to resolve a beneficiary's address/contact rows for the edit. - RmnchDataSyncServiceImpl: - extract occupation/community/pinCode/blockID from i_bendemographics, phoneNo from benPhoneMaps[0], and villageid/villagename directly (case-mismatch workaround) during the existing GPS-extraction pass - set community/occupation/religion onto the updated detail entity - new updateAddressAndContactOnEdit(): resolves the beneficiary's address/contact rows via i_beneficiarymapping and updates pinCode, block (via the already-correctly-bound facilitySelection), district, village, and phone -- only for fields the incoming payload actually sent, so an edit that only changes name/occupation can't blank out address data it didn't include Deliberately NOT fixed here: economicStatus. The app sends it, but no persisted column for it exists on the beneficiary record in this schema -- it appears to belong on the household record (i_householddetails.type_bpl_apl) instead. Persisting it onto the beneficiary would be guessing at the wrong data model without confirmation. Verified live end-to-end against a real edit (benRegId 45387): occupation, community, pincode, block, village, and phone all confirmed landing correctly in the database. Household edit has a separate, more severe bug (full-object overwrite silently wipes street/colony/village on any edit) -- out of scope for this commit, tracked separately. Co-Authored-By: Claude Sonnet 5 --- .../rmnch/RMNCHBeneficiaryDetailsRmnch.java | 12 ++ .../repo/rmnch/RMNCHMBenMappingRepo.java | 6 + .../rmnch/RmnchDataSyncServiceImpl.java | 156 +++++++++++++++++- 3 files changed, 169 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHBeneficiaryDetailsRmnch.java b/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHBeneficiaryDetailsRmnch.java index a6604f69..60805dcf 100644 --- a/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHBeneficiaryDetailsRmnch.java +++ b/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHBeneficiaryDetailsRmnch.java @@ -408,6 +408,18 @@ public class RMNCHBeneficiaryDetailsRmnch { @Expose @Transient private Integer communityId; + // Added for STOP-444 followup: edit sync silently dropped these — mobile sends them + // (see STOP-TB-App BenSending.kt) but this entity had no field to bind them to, so + // Gson/Jackson discarded them before they ever reached RmnchDataSyncServiceImpl. + @Expose + @Transient + private String occupation; + @Expose + @Transient + private Integer occupationId; + @Expose + @Transient + private String phoneNo; @Expose @Transient private String contact_number; diff --git a/src/main/java/com/iemr/common/identity/repo/rmnch/RMNCHMBenMappingRepo.java b/src/main/java/com/iemr/common/identity/repo/rmnch/RMNCHMBenMappingRepo.java index 18415e85..5827f9f1 100644 --- a/src/main/java/com/iemr/common/identity/repo/rmnch/RMNCHMBenMappingRepo.java +++ b/src/main/java/com/iemr/common/identity/repo/rmnch/RMNCHMBenMappingRepo.java @@ -40,6 +40,12 @@ public RMNCHMBeneficiarymapping getByIdAndVanID(@Param("vanSerialNo") BigInteger public RMNCHMBeneficiarymapping getByAddressIDAndVanID(@Param("addressID") BigInteger addressID, @Param("vanID") int vanID); + // Added for STOP-444 followup: resolve a beneficiary's address/contact IDs by BenRegId + // so an edit sync can update the existing address/contact rows instead of only + // i_beneficiarydetails. + @Query(" SELECT t FROM RMNCHMBeneficiarymapping t WHERE t.benRegId = :benRegId") + public RMNCHMBeneficiarymapping getByBenRegId(@Param("benRegId") BigInteger benRegId); + @Query(nativeQuery = true, value = " SELECT userid FROM db_iemr.m_user WHERE UserName = :userName ") public Integer getUserIDByUserName(@Param("userName") String userName); diff --git a/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java b/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java index aec3c44a..3a2674aa 100644 --- a/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java +++ b/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java @@ -186,14 +186,23 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex // Build GPS lookup map from i_bendemographics in raw JSON Map benGpsMap = new HashMap<>(); + // STOP-444 followup fix: also keep the raw top-level object per beneficiary — + // occupation/community/pinCode live inside i_bendemographics (only ever read + // for GPS before this fix), and phone number lives in benPhoneMaps[0], neither + // of which Gson could bind onto RMNCHBeneficiaryDetailsRmnch: the JSON here uses + // lowercase keys (e.g. "villageid") that don't case-match the Java fields, and + // i_bendemographics/benPhoneMaps are nested, not flat. + Map benRawMap = new HashMap<>(); JsonArray benJsonArr = jsnOBJ.getAsJsonArray("beneficiaryDetails"); for (JsonElement el : benJsonArr) { JsonObject benJson = el.getAsJsonObject(); - if (benJson.has("benficieryid") && !benJson.get("benficieryid").isJsonNull() - && benJson.has("i_bendemographics") - && !benJson.get("i_bendemographics").isJsonNull()) { - benGpsMap.put(benJson.get("benficieryid").getAsBigInteger(), - benJson.getAsJsonObject("i_bendemographics")); + if (benJson.has("benficieryid") && !benJson.get("benficieryid").isJsonNull()) { + benRawMap.put(benJson.get("benficieryid").getAsBigInteger(), benJson); + if (benJson.has("i_bendemographics") + && !benJson.get("i_bendemographics").isJsonNull()) { + benGpsMap.put(benJson.get("benficieryid").getAsBigInteger(), + benJson.getAsJsonObject("i_bendemographics")); + } } } @@ -213,6 +222,41 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex obj.setGpsTimestamp(new Timestamp(demog.get("gpsTimestamp").getAsLong())); if (demog.has("isGpsUnavailable") && !demog.get("isGpsUnavailable").isJsonNull()) obj.setIsGpsUnavailable(demog.get("isGpsUnavailable").getAsBoolean()); + // STOP-444 followup fix: same source object, previously never read for + // anything but GPS. economicStatus deliberately NOT extracted here — its + // correct persisted home is unconfirmed (looks like it belongs on the + // household record, i_householddetails.type_bpl_apl, not the beneficiary). + if (demog.has("occupation") && !demog.get("occupation").isJsonNull()) + obj.setOccupation(demog.get("occupation").getAsString()); + if (demog.has("communityName") && !demog.get("communityName").isJsonNull()) + obj.setCommunity(demog.get("communityName").getAsString()); + if (demog.has("communityID") && !demog.get("communityID").isJsonNull()) + obj.setCommunityId(demog.get("communityID").getAsInt()); + if (demog.has("pinCode") && !demog.get("pinCode").isJsonNull()) + obj.setPinCode(demog.get("pinCode").getAsString()); + if (demog.has("blockID") && !demog.get("blockID").isJsonNull()) + obj.setBlockId(demog.get("blockID").getAsInt()); + } + // STOP-444 followup fix: phone number is sent as benPhoneMaps[0].phoneNo, not + // a flat field — same lowercase-key mismatch problem as village/block below. + JsonObject benJsonForPhone = benRawMap.get(obj.getBenficieryid()); + if (benJsonForPhone != null && benJsonForPhone.has("benPhoneMaps") + && !benJsonForPhone.get("benPhoneMaps").isJsonNull()) { + JsonArray phoneMaps = benJsonForPhone.getAsJsonArray("benPhoneMaps"); + if (phoneMaps.size() > 0) { + JsonObject firstPhone = phoneMaps.get(0).getAsJsonObject(); + if (firstPhone.has("phoneNo") && !firstPhone.get("phoneNo").isJsonNull()) { + obj.setPhoneNo(firstPhone.get("phoneNo").getAsString()); + } + } + // STOP-444 followup fix: entity fields are villageId/villageName (camelCase) + // but the app sends lowercase "villageid"/"villagename" — Gson's default + // matching is case-sensitive, so these never bound at all. districtid/ + // districtname already happen to case-match and don't need this treatment. + if (benJsonForPhone.has("villageid") && !benJsonForPhone.get("villageid").isJsonNull()) + obj.setVillageId(benJsonForPhone.get("villageid").getAsInt()); + if (benJsonForPhone.has("villagename") && !benJsonForPhone.get("villagename").isJsonNull()) + obj.setVillageName(benJsonForPhone.get("villagename").getAsString()); } if(!rMNCHBeneficiaryDetailsRmnchRepo .getByRegID(benRegID).isEmpty()){ @@ -278,6 +322,26 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex rmnchmBeneficiarydetail.setPlaceOfCurrentLiving(obj.getPlaceOfCurrentLiving()); rmnchmBeneficiarydetail.setOtherPlaceOfCurrentLiving(obj.getOtherPlaceOfCurrentLiving()); rmnchmBeneficiarydetail.setInstitutionName(obj.getInstitutionName()); + // STOP-444 followup fix: these were previously never re-synced on an edit of an + // existing beneficiary, so any change to them from the app was silently dropped. + if (obj.getCommunity() != null) { + rmnchmBeneficiarydetail.setCommunity(obj.getCommunity()); + } + if (obj.getCommunityId() != null) { + rmnchmBeneficiarydetail.setCommunityId(obj.getCommunityId()); + } + if (obj.getOccupation() != null) { + rmnchmBeneficiarydetail.setOccupation(obj.getOccupation()); + } + if (obj.getOccupationId() != null) { + rmnchmBeneficiarydetail.setOccupationId(obj.getOccupationId()); + } + if (obj.getReligion() != null) { + rmnchmBeneficiarydetail.setReligion(obj.getReligion()); + } + if (obj.getReligionID() != null) { + rmnchmBeneficiarydetail.setReligionID(obj.getReligionID()); + } if(obj.getFamilyId()!=null && !obj.getFamilyId().isEmpty()){ rmnchmBeneficiarydetail.setFamilyId(obj.getFamilyId()); @@ -288,6 +352,17 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex } + // STOP-444 followup fix: pinCode/block/district/village/phone live on the + // address and contact tables, which this sync never touched on edit at all + // (only ever written at initial creation). Best-effort — only overwrites a + // field when the app actually sent a non-null value for it. + try { + updateAddressAndContactOnEdit(obj); + } catch (Exception ex) { + logger.warn("Failed to update address/contact on edit for benRegId: " + + obj.getBenRegId() + " - " + ex.getMessage()); + } + } } @@ -704,6 +779,77 @@ private boolean isPlausibleDeviceTimestamp(Timestamp createdDate) { return createdDate.getTime() > epochDayZero && createdDate.getTime() <= now; } + // STOP-444 followup fix: address (pinCode/block/district/village) and phone number + // live on i_beneficiaryaddress / i_beneficiarycontacts, which the edit-sync path never + // wrote to at all before this — only the initial create path (createIdentity) ever + // touched them. Best-effort: resolves the beneficiary's address/contact rows via + // i_beneficiarymapping and updates only the fields the app actually sent a value for, + // so an edit that only changes name/occupation doesn't blank out address fields the + // app didn't include in that particular payload. + private void updateAddressAndContactOnEdit(RMNCHBeneficiaryDetailsRmnch obj) { + if (obj.getBenRegId() == null || obj.getVanID() == null) { + return; + } + RMNCHMBeneficiarymapping mapping = rMNCHMBenMappingRepo.getByBenRegId(obj.getBenRegId()); + if (mapping == null) { + return; + } + int vanID = obj.getVanID(); + + if (mapping.getBenAddressId() != null) { + RMNCHMBeneficiaryaddress address = rMNCHBenAddressRepo.getByIdAndVanID(mapping.getBenAddressId(), vanID); + if (address != null) { + boolean changed = false; + if (obj.getPinCode() != null) { + address.setPermPinCode(obj.getPinCode()); + changed = true; + } + if (obj.getBlockId() != null) { + address.setPermSubDistrictId(obj.getBlockId()); + changed = true; + } + // No block NAME source verified in the real edit payload — demog only carries + // blockID (a number). facilitySelection is a real, correctly-bound column and is + // used as the block name elsewhere in this codebase for the same reason (see + // BenRepo.kt on the mobile side), so it's the one confirmed-correct source here. + if (obj.getFacilitySelection() != null) { + address.setPermSubDistrict(obj.getFacilitySelection()); + changed = true; + } + if (obj.getDistrictid() != null) { + address.setDistrictidPerm(obj.getDistrictid()); + changed = true; + } + if (obj.getDistrictname() != null) { + address.setDistrictnamePerm(obj.getDistrictname()); + changed = true; + } + if (obj.getVillageId() != null) { + address.setVillageidPerm(obj.getVillageId()); + changed = true; + } + if (obj.getVillageName() != null) { + address.setVillagenamePerm(obj.getVillageName()); + changed = true; + } + // zoneID / servicePointID / servicePointName dropped from this fix — no confirmed + // source key for these in a real captured edit payload, and guessing a wrong Gson + // binding here would look fixed while silently still doing nothing. + if (changed) { + rMNCHBenAddressRepo.save(address); + } + } + } + + if (mapping.getBenContactsId() != null && obj.getPhoneNo() != null) { + RMNCHMBeneficiarycontact contact = rMNCHBenContactRepo.getByIdAndVanID(mapping.getBenContactsId(), vanID); + if (contact != null) { + contact.setPreferredPhoneNum(obj.getPhoneNo()); + rMNCHBenContactRepo.save(contact); + } + } + } + private boolean hasAnthropometryData(RMNCHBeneficiaryDetailsRmnch obj) { return obj.getHeight() != null || obj.getWeight() != null || obj.getBmi() != null || obj.getTemperature() != null; From 3678bd7876f673c8cec67671bb5154bda9636485 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Fri, 11 Sep 2026 12:15:35 +0530 Subject: [PATCH 2/3] chore: bump version to 3.9.0 pom.xml still said 3.8.3 on this branch (cut from release-3.9.0) -- version string was never bumped when the branch was created. Co-Authored-By: Claude Sonnet 5 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 04fad9b0..9eda8258 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.iemr.common.identity identity-api - 3.8.3 + 3.9.0 war From cdc246eda9a35ec7e7b2facff83b8a3491511f13 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Fri, 11 Sep 2026 14:11:23 +0530 Subject: [PATCH 3/3] fix: sync beneficiary economicStatus/residentialArea on edit (STOP-444) Same class of bug as the earlier occupation/community fix, found while investigating two more fields reported not updating: "type of residential area" and "socioeconomic status". Both economicStatus/economicStatusId and residentialArea/residentialAreaId are real columns on i_beneficiarydetails -- confirmed 100% NULL across every row in the table, including freshly created ones, meaning no code path (create or edit) had ever written to them. The app already sends both values every sync, nested inside i_bendemographics. residentialArea/residentialAreaId were initially (incorrectly) placed on RMNCHMBeneficiaryaddress in an earlier pass of this fix -- caught and corrected by cross-checking against FLW-API's own RMNCHMBeneficiarydetail, which already declares both fields on i_beneficiarydetails. That mistake never shipped; caught before this branch was ever restarted with it. Every field touched across both fix commits (community, occupation, religion, economicStatus, residentialArea, pinCode, block, district, village, phone) has now been cross-verified against FLW-API's independently-built entity model for the same tables, not just assumed. Co-Authored-By: Claude Sonnet 5 --- .../rmnch/RMNCHBeneficiaryDetailsRmnch.java | 12 +++++++ .../data/rmnch/RMNCHMBeneficiarydetail.java | 18 +++++++++++ .../rmnch/RmnchDataSyncServiceImpl.java | 32 +++++++++++++++++-- 3 files changed, 59 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHBeneficiaryDetailsRmnch.java b/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHBeneficiaryDetailsRmnch.java index 60805dcf..b41c7ecc 100644 --- a/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHBeneficiaryDetailsRmnch.java +++ b/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHBeneficiaryDetailsRmnch.java @@ -422,6 +422,18 @@ public class RMNCHBeneficiaryDetailsRmnch { private String phoneNo; @Expose @Transient + private String economicStatus; + @Expose + @Transient + private Integer economicStatusId; + @Expose + @Transient + private String residentialArea; + @Expose + @Transient + private Integer residentialAreaId; + @Expose + @Transient private String contact_number; @Expose @Transient diff --git a/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHMBeneficiarydetail.java b/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHMBeneficiarydetail.java index 0e86127e..52ce4297 100644 --- a/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHMBeneficiarydetail.java +++ b/src/main/java/com/iemr/common/identity/data/rmnch/RMNCHMBeneficiarydetail.java @@ -111,6 +111,24 @@ public class RMNCHMBeneficiarydetail { @Column(length = 45) private String occupation; + // STOP-444 followup fix: column exists (economicStatus/economicStatusId on + // i_beneficiarydetails) and is 100% NULL across every row, including freshly + // created ones — confirmed no code path, create or edit, has ever written to it. + // The app sends this value (inside i_bendemographics) every time. + private Integer economicStatusId; + + @Column(length = 45) + private String economicStatus; + + // STOP-444 followup fix: same situation as economicStatus above — real column, + // never written. Originally (wrongly) placed on the address entity in an earlier + // pass; corrected here to match where FLW-API's own RMNCHMBeneficiarydetail already + // declares it, confirming this is the real table. + private Integer residentialAreaId; + + @Column(name = "residentialArea") + private String residentialArea; + private Integer phcId; @Column(length = 30) diff --git a/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java b/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java index 3a2674aa..6201d6a3 100644 --- a/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java +++ b/src/main/java/com/iemr/common/identity/service/rmnch/RmnchDataSyncServiceImpl.java @@ -223,9 +223,7 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex if (demog.has("isGpsUnavailable") && !demog.get("isGpsUnavailable").isJsonNull()) obj.setIsGpsUnavailable(demog.get("isGpsUnavailable").getAsBoolean()); // STOP-444 followup fix: same source object, previously never read for - // anything but GPS. economicStatus deliberately NOT extracted here — its - // correct persisted home is unconfirmed (looks like it belongs on the - // household record, i_householddetails.type_bpl_apl, not the beneficiary). + // anything but GPS. if (demog.has("occupation") && !demog.get("occupation").isJsonNull()) obj.setOccupation(demog.get("occupation").getAsString()); if (demog.has("communityName") && !demog.get("communityName").isJsonNull()) @@ -236,6 +234,18 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex obj.setPinCode(demog.get("pinCode").getAsString()); if (demog.has("blockID") && !demog.get("blockID").isJsonNull()) obj.setBlockId(demog.get("blockID").getAsInt()); + // STOP-444 followup fix: economicStatus previously skipped here — turned + // out i_beneficiarydetails.economicStatus/economicStatusId genuinely exist + // (confirmed via schema, not just source code — 100% NULL across every row + // including fresh ones), so this is the correct persisted home after all. + if (demog.has("economicStatus") && !demog.get("economicStatus").isJsonNull()) + obj.setEconomicStatus(demog.get("economicStatus").getAsString()); + if (demog.has("economicStatusId") && !demog.get("economicStatusId").isJsonNull()) + obj.setEconomicStatusId(demog.get("economicStatusId").getAsInt()); + if (demog.has("residentialArea") && !demog.get("residentialArea").isJsonNull()) + obj.setResidentialArea(demog.get("residentialArea").getAsString()); + if (demog.has("residentialAreaId") && !demog.get("residentialAreaId").isJsonNull()) + obj.setResidentialAreaId(demog.get("residentialAreaId").getAsInt()); } // STOP-444 followup fix: phone number is sent as benPhoneMaps[0].phoneNo, not // a flat field — same lowercase-key mismatch problem as village/block below. @@ -342,6 +352,22 @@ public String syncDataToAmrit(String requestOBJ, String authorization) throws Ex if (obj.getReligionID() != null) { rmnchmBeneficiarydetail.setReligionID(obj.getReligionID()); } + if (obj.getEconomicStatus() != null) { + rmnchmBeneficiarydetail.setEconomicStatus(obj.getEconomicStatus()); + } + if (obj.getEconomicStatusId() != null) { + rmnchmBeneficiarydetail.setEconomicStatusId(obj.getEconomicStatusId()); + } + // STOP-444 followup fix: residentialArea/residentialAreaId — corrected here + // from an earlier (wrong) placement on the address entity; confirmed against + // FLW-API's own RMNCHMBeneficiarydetail, which already declares these fields + // on this same table. + if (obj.getResidentialArea() != null) { + rmnchmBeneficiarydetail.setResidentialArea(obj.getResidentialArea()); + } + if (obj.getResidentialAreaId() != null) { + rmnchmBeneficiarydetail.setResidentialAreaId(obj.getResidentialAreaId()); + } if(obj.getFamilyId()!=null && !obj.getFamilyId().isEmpty()){ rmnchmBeneficiarydetail.setFamilyId(obj.getFamilyId());