From 49c9ddb3c30c4c6b4778025fbef42bdd7676ea29 Mon Sep 17 00:00:00 2001 From: Sehjot Singh Pannu Date: Thu, 18 Jun 2026 17:09:32 +0530 Subject: [PATCH] feat(STOP-148): add GPS location capture support for beneficiary and RMNCH records Introduces GPS-related fields (gpsLatitude, gpsLongitude, digipin, gpsTimestamp, isGpsUnavailable, and gpsUnavailableReason) across beneficiary address and RMNCH domain models, including MBeneficiaryaddress, Address DTO, RMNCHBeneficiaryDetailsRmnch, and RMNCHHouseHoldDetails. Enhances IdentityMapper and IdentityService to map and persist GPS information between incoming DTOs and beneficiary address entities. Updates RmnchDataSyncServiceImpl to extract and synchronize GPS details from the nested i_bendemographics payload during beneficiary sync, and to parse gpsTimestamp from household details during RMNCH household data processing. --- .../rmnch/RMNCHBeneficiaryDetailsRmnch.java | 24 ----------- .../common/identity/mapper/InputMapper.java | 43 ++++++++++++++++++- .../identity/utils/mapper/InputMapper.java | 35 +++++++++++++++ 3 files changed, 77 insertions(+), 25 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 55de4dc1..b964b25e 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 @@ -574,30 +574,6 @@ public class RMNCHBeneficiaryDetailsRmnch { @Column(name = "gpsUnavailableReason") private String gpsUnavailableReason; - @Expose - @Column(name = "gpsLatitude") - private Double gpsLatitude; - - @Expose - @Column(name = "gpsLongitude") - private Double gpsLongitude; - - @Expose - @Column(name = "digipin") - private String digipin; - - @Expose - @Column(name = "gpsTimestamp") - 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; - // Anthropometry fields sent by Stop TB mobile app via beneficiaryDetails payload. // i_beneficiarydetails_rmnch has no these columns — stored in i_beneficiarydetails.otherFields instead. @Expose diff --git a/src/main/java/com/iemr/common/identity/mapper/InputMapper.java b/src/main/java/com/iemr/common/identity/mapper/InputMapper.java index 9a46ba4f..704fe671 100644 --- a/src/main/java/com/iemr/common/identity/mapper/InputMapper.java +++ b/src/main/java/com/iemr/common/identity/mapper/InputMapper.java @@ -21,6 +21,13 @@ */ package com.iemr.common.identity.mapper; +import java.io.IOException; +import java.sql.Timestamp; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Locale; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,6 +40,10 @@ import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; import com.google.gson.LongSerializationPolicy; +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; import com.iemr.common.identity.exception.IEMRException; public class InputMapper @@ -42,11 +53,41 @@ public class InputMapper private static GsonBuilder builder; private static InputMapper instance = null; + private static final DateTimeFormatter ISO_WITH_Z = + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); + private static final DateTimeFormatter CLIENT_DATE_FORMAT = + DateTimeFormatter.ofPattern("MMM dd, yyyy, h:mm:ss a", Locale.ENGLISH); + + private static final TypeAdapter TIMESTAMP_ADAPTER = new TypeAdapter() { + @Override + public void write(JsonWriter out, Timestamp value) throws IOException { + if (value == null) out.nullValue(); + else out.value(value.getTime()); + } + + @Override + public Timestamp read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { in.nextNull(); return null; } + if (in.peek() == JsonToken.NUMBER) return new Timestamp(in.nextLong()); + String s = in.nextString(); + // epoch millis as string + try { return new Timestamp(Long.parseLong(s)); } catch (NumberFormatException ignored) {} + // ISO 8601 with Z e.g. "2021-06-18T00:00:00.000Z" + try { return Timestamp.from(Instant.parse(s)); } catch (Exception ignored) {} + // Mobile client format e.g. "Jun 18, 2021, 5:30:00 AM" + try { return Timestamp.valueOf(LocalDateTime.parse(s, CLIENT_DATE_FORMAT)); } catch (Exception ignored) {} + // Fallback: "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" literal Z + try { return Timestamp.valueOf(LocalDateTime.parse(s, ISO_WITH_Z)); } catch (Exception ignored) {} + return null; + } + }; + private InputMapper() { builder = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") // .excludeFieldsWithoutExposeAnnotation() - .serializeNulls().setLongSerializationPolicy(LongSerializationPolicy.STRING); + .serializeNulls().setLongSerializationPolicy(LongSerializationPolicy.STRING) + .registerTypeAdapter(Timestamp.class, TIMESTAMP_ADAPTER); } public static InputMapper getInstance() diff --git a/src/main/java/com/iemr/common/identity/utils/mapper/InputMapper.java b/src/main/java/com/iemr/common/identity/utils/mapper/InputMapper.java index 4c8db37d..fb419105 100644 --- a/src/main/java/com/iemr/common/identity/utils/mapper/InputMapper.java +++ b/src/main/java/com/iemr/common/identity/utils/mapper/InputMapper.java @@ -21,6 +21,13 @@ */ package com.iemr.common.identity.utils.mapper; +import java.io.IOException; +import java.sql.Timestamp; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @@ -28,6 +35,10 @@ import com.google.gson.ExclusionStrategy; import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; import com.iemr.common.identity.utils.exception.IEMRException; /** @@ -48,6 +59,30 @@ public InputMapper() { if (builder == null) { builder = new GsonBuilder(); builder.setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"); + builder.registerTypeAdapter(Timestamp.class, new TypeAdapter() { + private final DateTimeFormatter isoWithZ = DateTimeFormatter.ISO_INSTANT; + private final DateTimeFormatter isoNoTz = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS"); + + @Override + public void write(JsonWriter out, Timestamp value) throws IOException { + if (value == null) out.nullValue(); + else out.value(value.getTime()); + } + + @Override + public Timestamp read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { in.nextNull(); return null; } + if (in.peek() == JsonToken.NUMBER) return new Timestamp(in.nextLong()); + String s = in.nextString(); + // epoch millis as string + try { return new Timestamp(Long.parseLong(s)); } catch (NumberFormatException ignored) {} + // ISO 8601 with timezone (e.g. "2026-05-28T03:24:35.000Z") + try { return Timestamp.from(Instant.parse(s)); } catch (Exception ignored) {} + // ISO without timezone (e.g. "2026-05-28T03:24:35.000") + try { return Timestamp.from(LocalDateTime.parse(s, isoNoTz).toInstant(ZoneOffset.UTC)); } catch (Exception ignored) {} + return null; + } + }); } }