From 9efa2a3c89d1c493542b49e1bd820145c0ac1a69 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E2=80=9CKomal?= <“komal_m@tekditechnologies.com”>
Date: Mon, 19 Jan 2026 15:58:37 +0530
Subject: [PATCH 01/22] Task #252259 feat: Created new Job for - When the
household profile Observation is submitted, participant's profile should be
updated
---
Documentation/Docker-setup/config.env | 6 +-
Documentation/Docker-setup/docker-compose.yml | 27 +--
jobs-core/src/main/resources/base-config.conf | 6 +-
.../config-data-loader/data-loader.sh | 6 +-
.../resources/metabase-project-dashboard.conf | 6 +-
.../src/test/resources/test.conf | 16 +-
stream-jobs/pom.xml | 1 +
.../src/main/resources/project-stream.conf | 6 +-
.../src/test/resources/test.conf | 6 +-
.../user-mapping-stream-processor/pom.xml | 220 ++++++++++++++++++
.../src/main/resources/field-mappings.conf | 3 +
.../main/resources/user-mapping-stream.conf | 27 +++
.../stream/processor/domain/Event.scala | 132 +++++++++++
.../processor/domain/ObservationEvent.scala | 74 ++++++
.../functions/UserMappingStreamFunction.scala | 112 +++++++++
.../task/UserMappingStreamConfig.scala | 145 ++++++++++++
.../task/UserMappingStreamTask.scala | 60 +++++
.../stream/processor/util/FieldMapper.scala | 156 +++++++++++++
.../stream/processor/util/UserApiClient.scala | 124 ++++++++++
.../src/test/resources/test.conf | 26 +++
.../fixture/ObservationEventsMock.scala | 37 +++
.../processor/spec/GenerateUserSink.scala | 19 ++
.../spec/ObservationEventSource.scala | 17 ++
.../UserMappingStreamFunctionTestSpec.scala | 59 +++++
24 files changed, 1252 insertions(+), 39 deletions(-)
create mode 100644 stream-jobs/user-mapping-stream-processor/pom.xml
create mode 100644 stream-jobs/user-mapping-stream-processor/src/main/resources/field-mappings.conf
create mode 100644 stream-jobs/user-mapping-stream-processor/src/main/resources/user-mapping-stream.conf
create mode 100644 stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/domain/Event.scala
create mode 100644 stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/domain/ObservationEvent.scala
create mode 100644 stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/functions/UserMappingStreamFunction.scala
create mode 100644 stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/task/UserMappingStreamConfig.scala
create mode 100644 stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/task/UserMappingStreamTask.scala
create mode 100644 stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/util/FieldMapper.scala
create mode 100644 stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/util/UserApiClient.scala
create mode 100644 stream-jobs/user-mapping-stream-processor/src/test/resources/test.conf
create mode 100644 stream-jobs/user-mapping-stream-processor/src/test/scala/org/shikshalokam/user/mapping/stream/processor/fixture/ObservationEventsMock.scala
create mode 100644 stream-jobs/user-mapping-stream-processor/src/test/scala/org/shikshalokam/user/mapping/stream/processor/spec/GenerateUserSink.scala
create mode 100644 stream-jobs/user-mapping-stream-processor/src/test/scala/org/shikshalokam/user/mapping/stream/processor/spec/ObservationEventSource.scala
create mode 100644 stream-jobs/user-mapping-stream-processor/src/test/scala/org/shikshalokam/user/mapping/stream/processor/spec/UserMappingStreamFunctionTestSpec.scala
diff --git a/Documentation/Docker-setup/config.env b/Documentation/Docker-setup/config.env
index 91c38a33..a4e9b4d0 100644
--- a/Documentation/Docker-setup/config.env
+++ b/Documentation/Docker-setup/config.env
@@ -1,11 +1,11 @@
# Postgres
POSTGRES_USER=postgres
-POSTGRES_PASSWORD=password
+POSTGRES_PASSWORD=1234
POSTGRES_PORT=5432 #use this for docker
POSTGRES_HOST=postgres #use this for docker
# PGADMIN
-PGADMIN_DEFAULT_EMAIL=admin@example.com
+PGADMIN_DEFAULT_EMAIL=komal_m@tekditechnologies.com
PGADMIN_DEFAULT_PASSWORD=admin
# METABASE
@@ -29,4 +29,4 @@ ELEVATE_DATA_CONTAINER_NAME=elevate-data
# Kafka-Topics
PROJECT_TOPIC=sl-improvement-project-submission-dev
-METABASE_TOPIC=sl-improvement-metabase-dev
\ No newline at end of file
+METABASE_TOPIC=sl-improvement-metabase-dev
diff --git a/Documentation/Docker-setup/docker-compose.yml b/Documentation/Docker-setup/docker-compose.yml
index f9a9f2ea..db07c97f 100644
--- a/Documentation/Docker-setup/docker-compose.yml
+++ b/Documentation/Docker-setup/docker-compose.yml
@@ -78,13 +78,13 @@ services:
restart: always
container_name: postgres
environment:
- POSTGRES_USER: ${POSTGRES_USER}
- POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
- POSTGRES_DB: ${POSTGRES_DB}
+ POSTGRES_USER: postgres
+ POSTGRES_PASSWORD: M3TabAse#321
+ POSTGRES_DB: metabase
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- - "5432:5432"
+ - "5533:5432"
networks:
- elevate_net
@@ -111,13 +111,14 @@ services:
restart: always
container_name: metabase
environment:
- MB_DB_TYPE: ${MB_DB_TYPE}
- MB_DB_DBNAME: ${POSTGRES_DB}
- MB_DB_PORT: ${POSTGRES_PORT}
- MB_DB_USER: ${POSTGRES_USER}
- MB_DB_PASS: ${POSTGRES_PASSWORD}
- MB_DB_HOST: ${POSTGRES_HOST}
- MB_API_KEY: ${MB_API_KEY}
+ MB_DB_TYPE: "postgres"
+ MB_DB_DBNAME: "metabase_elevate"
+ MB_DB_PORT: "5432"
+ MB_DB_USER: "postgres"
+# MB_DB_PASS: "M3TabAse#321"
+ MB_DB_PASS: "1234"
+ MB_DB_HOST: "172.132.44.221"
+ MB_API_KEY: "9c2a8e4b-6f1d-3c5d-7e8f-1a2b3c4d5e6f"
ports:
- "3000:3000"
depends_on:
@@ -129,7 +130,7 @@ services:
# Elevate Data Service
elevate-data:
- image: shikshalokamqa/elevate-data:latest
+ image: shikshalokamqa/elevate-data:v3.0.2
restart: always
container_name: elevate-data
ports:
@@ -178,4 +179,4 @@ volumes:
elevate_data:
driver: local
kafka_data:
- driver: local
\ No newline at end of file
+ driver: local
diff --git a/jobs-core/src/main/resources/base-config.conf b/jobs-core/src/main/resources/base-config.conf
index 93625de1..0ac79975 100644
--- a/jobs-core/src/main/resources/base-config.conf
+++ b/jobs-core/src/main/resources/base-config.conf
@@ -1,6 +1,6 @@
kafka {
- broker-servers = "10.148.0.38:9092"
- zookeeper = "10.148.0.38:2181"
+ broker-servers = "user-kafka-1:9092"
+ zookeeper = "zookeeper:2181"
}
job {
@@ -27,4 +27,4 @@ task {
restart-strategy.attempts = 3
restart-strategy.delay = 30000 # in milli-seconds
consumer.parallelism = 1
-}
\ No newline at end of file
+}
diff --git a/metabase-jobs/config-data-loader/data-loader.sh b/metabase-jobs/config-data-loader/data-loader.sh
index 76753f68..d1dbda1d 100755
--- a/metabase-jobs/config-data-loader/data-loader.sh
+++ b/metabase-jobs/config-data-loader/data-loader.sh
@@ -1,9 +1,9 @@
#!/bin/bash
# Database connection parameters
-DB_NAME="postgres"
+DB_NAME="elevate_data"
DB_USER="postgres"
-DB_PASSWORD="postgres"
+DB_PASSWORD="1234"
DB_HOST="localhost"
DB_PORT="5432"
TABLE_NAME="local_report_config"
@@ -75,7 +75,7 @@ process_folders() {
}
# Main folder path
-MAIN_FOLDER="/home/user2/Documents/elevate/data-pipeline/metabase-jobs/config-data-loader/projectJson"
+MAIN_FOLDER="/home/ttpl-rt-221/elevate/data-pipeline/metabase-jobs/config-data-loader/projectJson"
# Create the table and process folders
create_table
diff --git a/metabase-jobs/project-dashboard-creator/src/main/resources/metabase-project-dashboard.conf b/metabase-jobs/project-dashboard-creator/src/main/resources/metabase-project-dashboard.conf
index 9dbcad79..18e1b138 100644
--- a/metabase-jobs/project-dashboard-creator/src/main/resources/metabase-project-dashboard.conf
+++ b/metabase-jobs/project-dashboard-creator/src/main/resources/metabase-project-dashboard.conf
@@ -12,8 +12,8 @@ postgres{
host = "localhost"
port = "5432"
username = "postgres"
- password = "postgres"
- database = "test"
+ password = "1234"
+ database = "elevate_data"
metabaseDb = "metabase"
tables = {
solutionsTable = ${job.env}"_solutions"
@@ -32,4 +32,4 @@ metabase {
domainName = "http://localhost:3000/dashboard/"
metabaseApiKey = "d3f4ult-api-key"
evidenceBaseUrl = "https://elevate-api.sunbirdsaas.com/project/v1/cloud-services/files/download?file="
-}
\ No newline at end of file
+}
diff --git a/metabase-jobs/project-dashboard-creator/src/test/resources/test.conf b/metabase-jobs/project-dashboard-creator/src/test/resources/test.conf
index 467347ba..abde8313 100644
--- a/metabase-jobs/project-dashboard-creator/src/test/resources/test.conf
+++ b/metabase-jobs/project-dashboard-creator/src/test/resources/test.conf
@@ -13,9 +13,9 @@ postgres{
host = "localhost"
port = "5432"
username = "postgres"
- password = "postgres"
- database = "test"
- metabaseDb = "metabase"
+ password = "1234"
+ database = "elevate_data"
+ metabaseDb = "metabase_elevate"
tables = {
solutionsTable = ${job.env}"_solutions"
projectsTable = ${job.env}"_projects"
@@ -27,10 +27,10 @@ postgres{
metabase {
url = "http://localhost:3000/api"
- username = "vivek@shikshalokam.org"
- password = "Test@1234"
- database = "test"
+ username = "komal@yopmail.com"
+ password = "pw4komal"
+ database = "Postgres"
domainName = "http://localhost:3000/dashboard/"
- metabaseApiKey = "d3f4ult-api-key"
+ metabaseApiKey = "9c2a8e4b-6f1d-3c5d-7e8f-1a2b3c4d5e6f"
evidenceBaseUrl = "https://TESTING="
-}
\ No newline at end of file
+}
diff --git a/stream-jobs/pom.xml b/stream-jobs/pom.xml
index d5be4f07..afcf04fb 100644
--- a/stream-jobs/pom.xml
+++ b/stream-jobs/pom.xml
@@ -20,6 +20,7 @@
survey-stream-processor
observation-stream-processor
user-stream-processor
+ user-mapping-stream-processor
mentoring-stream-processor
diff --git a/stream-jobs/project-stream-processor/src/main/resources/project-stream.conf b/stream-jobs/project-stream-processor/src/main/resources/project-stream.conf
index 35a3d1c1..c5455290 100644
--- a/stream-jobs/project-stream-processor/src/main/resources/project-stream.conf
+++ b/stream-jobs/project-stream-processor/src/main/resources/project-stream.conf
@@ -16,8 +16,8 @@ postgres{
host = "localhost"
port = "5432"
username = "postgres"
- password = "postgres"
- database = "test"
+ password = "1234"
+ database = "elevate_data"
tables = {
solutionsTable = ${job.env}"_solutions"
projectsTable = ${job.env}"_projects"
@@ -28,4 +28,4 @@ postgres{
reports{
enabled = ["admin", "state", "district", "program", "solution"]
-}
\ No newline at end of file
+}
diff --git a/stream-jobs/project-stream-processor/src/test/resources/test.conf b/stream-jobs/project-stream-processor/src/test/resources/test.conf
index efe92aee..4b41f13f 100644
--- a/stream-jobs/project-stream-processor/src/test/resources/test.conf
+++ b/stream-jobs/project-stream-processor/src/test/resources/test.conf
@@ -15,8 +15,8 @@ postgres{
host = "localhost"
port = "5432"
username = "postgres"
- password = "postgres"
- database = "test"
+ password = "1234"
+ database = "elevate_data"
tables = {
solutionsTable = ${job.env}"_solutions"
projectsTable = ${job.env}"_projects"
@@ -27,4 +27,4 @@ postgres{
reports{
enabled = ["admin", "state", "district", "program", "solution"]
-}
\ No newline at end of file
+}
diff --git a/stream-jobs/user-mapping-stream-processor/pom.xml b/stream-jobs/user-mapping-stream-processor/pom.xml
new file mode 100644
index 00000000..4ad2aa1c
--- /dev/null
+++ b/stream-jobs/user-mapping-stream-processor/pom.xml
@@ -0,0 +1,220 @@
+
+
+
+ 4.0.0
+
+ org.shikshalokam
+ data-pipeline
+ 1.0
+ ../../pom.xml
+
+ user-mapping-stream-processor
+ 1.0.0
+ jar
+ UserMappingStreamProcessor
+
+ Job for stream processing user mapping events
+
+
+
+ UTF-8
+ 1.4.0
+
+
+
+
+
+ org.shikshalokam
+ jobs-core
+ 1.0.0
+
+
+
+ org.shikshalokam
+ jobs-core
+ 1.0.0
+ test-jar
+ test
+
+
+
+
+ org.apache.flink
+ flink-scala_${scala.version}
+ ${flink.version}
+ provided
+
+
+ org.apache.flink
+ flink-streaming-scala_${scala.version}
+ ${flink.version}
+
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.15.3
+
+
+
+
+ org.scalatest
+ scalatest_${scala.version}
+ 3.0.6
+ test
+
+
+ org.apache.flink
+ flink-test-utils_${scala.version}
+ ${flink.version}
+ test
+
+
+ org.apache.flink
+ flink-runtime_${scala.version}
+ ${flink.version}
+ test
+ tests
+
+
+ org.mockito
+ mockito-core
+ 4.4.0
+ test
+
+
+
+
+
+ src/main/scala
+ src/test/scala
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.8.1
+
+ 11
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.2.1
+
+
+
+ package
+
+ shade
+
+
+
+
+ com.google.code.findbugs:jsr305
+
+
+
+
+
+ *:*
+
+ META-INF/*.SF
+ META-INF/*.DSA
+ META-INF/*.RSA
+
+
+
+
+
+
+ org.shikshalokam.job.user.mapping.stream.processor.task.UserMappingStreamTask
+
+
+
+ reference.conf
+
+
+
+
+
+
+
+
+ net.alchim31.maven
+ scala-maven-plugin
+ 4.4.0
+
+ ${java.target.runtime}
+ ${java.target.runtime}
+ ${scala.maj.version}
+ false
+
+
+
+ scala-compile-first
+ process-resources
+
+ add-source
+ compile
+
+
+
+ scala-test-compile
+ process-test-resources
+
+ testCompile
+
+
+
+
+
+
+ maven-surefire-plugin
+ ${surefire.plugin.version}
+
+ false
+
+
+
+
+ org.scalatest
+ scalatest-maven-plugin
+ 1.0
+
+ ${project.build.directory}/surefire-reports
+ .
+ user-mapping-jobs-testsuite.txt
+
+
+
+ test
+
+ test
+
+
+
+
+
+
+ org.scoverage
+ scoverage-maven-plugin
+ ${scoverage.plugin.version}
+
+ ${scala.version}
+ true
+ true
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/stream-jobs/user-mapping-stream-processor/src/main/resources/field-mappings.conf b/stream-jobs/user-mapping-stream-processor/src/main/resources/field-mappings.conf
new file mode 100644
index 00000000..81d78158
--- /dev/null
+++ b/stream-jobs/user-mapping-stream-processor/src/main/resources/field-mappings.conf
@@ -0,0 +1,3 @@
+name = profile.name
+about = profile.about
+dob = profile.dob
\ No newline at end of file
diff --git a/stream-jobs/user-mapping-stream-processor/src/main/resources/user-mapping-stream.conf b/stream-jobs/user-mapping-stream-processor/src/main/resources/user-mapping-stream.conf
new file mode 100644
index 00000000..e50bc344
--- /dev/null
+++ b/stream-jobs/user-mapping-stream-processor/src/main/resources/user-mapping-stream.conf
@@ -0,0 +1,27 @@
+include "base-config.conf"
+
+kafka {
+ input.topic = "dev.userCreate"
+ groupId = "dev.users"
+ output.topic = "sl-metabase-user-dashboard-dev"
+ output.mentoring.topic = "sl-metabase-mentoring-dashboard-dev"
+}
+
+task {
+ consumer.parallelism = 1
+ sl.users.stream.parallelism = 1
+ sl.metabase.dashboard.parallelism = 1
+}
+
+postgres{
+ host = "localhost"
+ port = "5432"
+ username = "postgres"
+ password = "1234"
+ database = "elevate_data"
+ tables = {
+ userMetrics = ${job.env}"_user_metrics"
+ dashboardMetadataTable = ${job.env}"_dashboard_metadata"
+ }
+}
+
diff --git a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/domain/Event.scala b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/domain/Event.scala
new file mode 100644
index 00000000..e95fdce7
--- /dev/null
+++ b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/domain/Event.scala
@@ -0,0 +1,132 @@
+package org.shikshalokam.job.user.mapping.stream.processor.domain
+
+import org.shikshalokam.job.domain.reader.JobRequest
+
+import java.sql.Timestamp
+import java.text.SimpleDateFormat
+import java.time.Instant
+import scala.language.postfixOps
+
+class Event(eventMap: java.util.Map[String, Any], partition: Int, offset: Long) extends JobRequest(eventMap, partition, offset) {
+
+ def eventType: String = readOrDefault[String]("eventType", null)
+
+ def userId: Int = readOrDefault[Int]("entityId", -1)
+
+ def tenantCode: String = extractValue[String]("tenant_code").orNull
+
+ def username: String = extractValue[String]("username").orNull
+
+ def name: String = extractValue[String]("name").orNull
+
+ def status: String = extractValue[String]("status").orNull
+
+ def isDeleted: Boolean = extractValue[Boolean]("deleted").getOrElse(false)
+
+ def createdBy: Int = extractValue[Int]("created_by").getOrElse(-1)
+
+ def createdAt: Timestamp = parseTimestamp(extractValue[Any]("created_at").orNull)
+
+ def updatedAt: Timestamp = parseTimestamp(extractValue[Any]("updated_at").orNull)
+
+ def userProfileOneId: String = extractValue[String]("state.id").orNull
+
+ def userProfileOneName: String = extractValue[String]("state.name").orNull
+
+ def userProfileOneExternalId: String = extractValue[String]("state.externalId").orNull
+
+ def userProfileTwoId: String = extractValue[String]("district.id").orNull
+
+ def userProfileTwoName: String = extractValue[String]("district.name").orNull
+
+ def userProfileTwoExternalId: String = extractValue[String]("district.externalId").orNull
+
+ def userProfileThreeId: String = extractValue[String]("block.id").orNull
+
+ def userProfileThreeName: String = extractValue[String]("block.name").orNull
+
+ def userProfileThreeExternalId: String = extractValue[String]("block.externalId").orNull
+
+ def userProfileFourId: String = extractValue[String]("cluster.id").orNull
+
+ def userProfileFourName: String = extractValue[String]("cluster.name").orNull
+
+ def userProfileFourExternalId: String = extractValue[String]("cluster.externalId").orNull
+
+ def userProfileFiveId: String = extractValue[String]("school.id").orNull
+
+ def userProfileFiveName: String = extractValue[String]("school.name").orNull
+
+ def userProfileFiveExternalId: String = extractValue[String]("school.externalId").orNull
+
+ def organizations: List[Map[String, Any]] = {
+ val fromDefault = Option(readOrDefault[List[Map[String, Any]]]("organizations", null)).getOrElse(List.empty)
+
+ var fromNew: List[Map[String, Any]] = List.empty
+ var fromOld: List[Map[String, Any]] = List.empty
+
+ if (eventType == "update" || eventType == "bulk-update") {
+ fromNew = Option(readOrDefault[List[Map[String, Any]]]("newValues.organizations", null)).getOrElse(List.empty)
+ fromOld = Option(readOrDefault[List[Map[String, Any]]]("oldValues.organizations", null)).getOrElse(List.empty)
+ }
+
+ (fromDefault ++ fromOld ++ fromNew).distinct
+ }
+
+ def professionalRoleId: String = extractNestedValue[String]("professional_role", "id").orNull
+
+ def professionalRoleName: String = extractNestedValue[String]("professional_role", "name").orNull
+
+ def professionalSubroles: List[Map[String, Any]] = {
+ val defaultList = Option(readOrDefault[List[Map[String, Any]]]("professional_subroles", null)).getOrElse(List.empty)
+ val newList = if (isUpdateEvent) Option(readOrDefault[List[Map[String, Any]]]("newValues.professional_subroles", null)).getOrElse(List.empty) else List.empty
+ val oldList = if (isUpdateEvent) Option(readOrDefault[List[Map[String, Any]]]("oldValues.professional_subroles", null)).getOrElse(List.empty) else List.empty
+
+ (defaultList ++ oldList ++ newList).distinct
+ }
+
+ private def extractValue[T](key: String): Option[T] = {
+ val direct = Option(readOrDefault[T](key, null.asInstanceOf[T]))
+
+ var fromNew: Option[T] = None
+ var fromOld: Option[T] = None
+
+ if (eventType == "update" || eventType == "bulk-update") {
+ fromNew = Option(readOrDefault[T](s"newValues.$key", null.asInstanceOf[T]))
+ fromOld = Option(readOrDefault[T](s"oldValues.$key", null.asInstanceOf[T]))
+ }
+
+ (direct orElse fromNew orElse fromOld).filter(_ != null)
+ }
+
+ private val isUpdateEvent: Boolean = eventType == "update" || eventType == "bulk-update"
+
+ private def extractNestedValue[T](base: String, key: String): Option[T] = {
+ val defaultMap = Option(readOrDefault[Map[String, Any]](base, null)).getOrElse(Map.empty)
+ val newMap = if (isUpdateEvent) Option(readOrDefault[Map[String, Any]](s"newValues.$base", null)).getOrElse(Map.empty) else Map.empty
+ val oldMap = if (isUpdateEvent) Option(readOrDefault[Map[String, Any]](s"oldValues.$base", null)).getOrElse(Map.empty) else Map.empty
+
+ val combined = defaultMap ++ oldMap ++ newMap
+ Option(combined.getOrElse(key, null).asInstanceOf[T]).filter(_ != null)
+ }
+
+ private def parseTimestamp(value: Any): Timestamp = value match {
+ case ts: Timestamp => ts
+ case s: String if s.trim.nonEmpty =>
+ try {
+ Timestamp.valueOf(s)
+ } catch {
+ case _: IllegalArgumentException =>
+ try {
+ Timestamp.from(Instant.parse(s))
+ } catch {
+ case _: Exception =>
+ val formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
+ new Timestamp(formatter.parse(s).getTime)
+ }
+ }
+ case _ => new Timestamp(System.currentTimeMillis())
+ }
+
+}
+
diff --git a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/domain/ObservationEvent.scala b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/domain/ObservationEvent.scala
new file mode 100644
index 00000000..a5120e0d
--- /dev/null
+++ b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/domain/ObservationEvent.scala
@@ -0,0 +1,74 @@
+package org.shikshalokam.job.user.mapping.stream.processor.domain
+
+import org.shikshalokam.job.domain.reader.JobRequest
+import ujson.Js
+
+import java.util
+import scala.collection.JavaConverters._
+
+/**
+ * Case class to represent observation submission events from Kafka
+ *
+ * Sample event structure:
+ * {
+ * "eventType": "observation-submitted",
+ * "id": 3088,
+ * "organizationId": 1,
+ * "observationData": {
+ * "name": "Carol Miranda Updated Two",
+ * "about": "admin Update",
+ * "dob": "22-12-1990"
+ * }
+ * }
+ */
+class ObservationEvent(eventMap: java.util.Map[String, Any], partition: Int, offset: Long)
+ extends JobRequest(eventMap, partition, offset) {
+
+ def eventType: String = readOrDefault[String]("eventType", null)
+
+ def studentId: String = {
+ // Support both "id" (numeric) and "studentId" (string) for backward compatibility
+ val idValue = readOrDefault[Any]("id", null)
+ val studentIdValue = readOrDefault[String]("studentId", null)
+
+ if (idValue != null) {
+ // Convert numeric ID to string
+ idValue match {
+ case n: Number => n.toString
+ case s: String => s
+ case _ => idValue.toString
+ }
+ } else if (studentIdValue != null) {
+ studentIdValue
+ } else {
+ null
+ }
+ }
+
+ def organizationId: Long = {
+ val value = readOrDefault[Any]("organizationId", null)
+ value match {
+ case l: Long => l
+ case i: Int => i.toLong
+ case n: Number => n.longValue()
+ case _ => -1L
+ }
+ }
+
+ def observationData: util.Map[String, Any] = {
+ val data = readOrDefault[Any]("observationData", null)
+ if (data == null) {
+ new util.HashMap[String, Any]()
+ } else {
+ data match {
+ case javaMap: util.Map[String, Any] => javaMap
+ case scalaMap: scala.collection.Map[String, Any] => scalaMap.asJava
+ case _ => new util.HashMap[String, Any]()
+ }
+ }
+ }
+
+ override def toString: String = {
+ s"ObservationEvent(eventType=$eventType, studentId=$studentId, organizationId=$organizationId)"
+ }
+}
diff --git a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/functions/UserMappingStreamFunction.scala b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/functions/UserMappingStreamFunction.scala
new file mode 100644
index 00000000..e5a308d0
--- /dev/null
+++ b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/functions/UserMappingStreamFunction.scala
@@ -0,0 +1,112 @@
+package org.shikshalokam.job.user.mapping.stream.processor.functions
+
+import org.apache.flink.api.common.typeinfo.TypeInformation
+import org.apache.flink.configuration.Configuration
+import org.apache.flink.streaming.api.functions.ProcessFunction
+import org.shikshalokam.job.user.mapping.stream.processor.domain.ObservationEvent
+import org.shikshalokam.job.user.mapping.stream.processor.task.UserMappingStreamConfig
+import org.shikshalokam.job.user.mapping.stream.processor.util.{FieldMapper, UserApiClient}
+import org.shikshalokam.job.{BaseProcessFunction, Metrics}
+import org.slf4j.LoggerFactory
+
+import java.util
+import scala.collection.JavaConverters._
+import scala.util.{Failure, Success}
+
+class UserMappingStreamFunction(config: UserMappingStreamConfig)(implicit val mapTypeInfo: TypeInformation[ObservationEvent])
+ extends BaseProcessFunction[ObservationEvent, ObservationEvent](config) {
+
+ private[this] val logger = LoggerFactory.getLogger(classOf[UserMappingStreamFunction])
+
+ override def metricsList(): List[String] = {
+ List(config.skipCount, config.successCount, config.totalEventsCount)
+ }
+
+ override def open(parameters: Configuration): Unit = {
+ super.open(parameters)
+ println("[UserMappingStreamFunction] Initializing...")
+ println("[UserMappingStreamFunction] FieldMapper mappings loaded")
+ FieldMapper.getMappings.foreach { case (source, target) =>
+ println(s"[UserMappingStreamFunction] Mapping: $source -> $target")
+ }
+ }
+
+ override def close(): Unit = {
+ super.close()
+ println("[UserMappingStreamFunction] Closing...")
+ }
+
+ override def processElement(event: ObservationEvent, context: ProcessFunction[ObservationEvent, ObservationEvent]#Context, metrics: Metrics): Unit = {
+
+ try {
+ println(s"***************** Start Processing Observation Event *****************")
+ println(s"[UserMappingStreamFunction] Event Type: ${event.eventType}")
+ println(s"[UserMappingStreamFunction] Student ID: ${event.studentId}")
+ println(s"[UserMappingStreamFunction] Organization ID: ${event.organizationId}")
+
+ // Update total events count metric
+ metrics.incCounter(config.totalEventsCount)
+
+ // Validate event
+ val studentId = event.studentId
+ if (studentId == null || studentId.trim.isEmpty) {
+ println(s"[UserMappingStreamFunction] ERROR: studentId/id is null or empty, skipping event")
+ logger.error("[UserMappingStreamFunction] studentId/id is null or empty")
+ metrics.incCounter(config.skipCount)
+ return
+ }
+
+ if (event.observationData == null || event.observationData.isEmpty) {
+ println(s"[UserMappingStreamFunction] WARNING: observationData is null or empty for studentId=$studentId")
+ logger.warn(s"[UserMappingStreamFunction] observationData is null or empty for studentId=$studentId")
+ metrics.incCounter(config.skipCount)
+ return
+ }
+
+ // Extract observation data
+ val observationData = event.observationData
+ println(s"[UserMappingStreamFunction] Observation data keys: ${observationData.keySet().asScala.mkString(", ")}")
+
+ // Transform observation data to profile format using FieldMapper
+ println(s"[UserMappingStreamFunction] Transforming observation data to profile format...")
+ val profileData = FieldMapper.transform(observationData)
+
+ // Check if profile data is empty
+ val profileObj = profileData.value.get("profile")
+ if (profileObj.isEmpty || profileObj.get.asInstanceOf[ujson.Obj].value.isEmpty) {
+ println(s"[UserMappingStreamFunction] WARNING: No profile data to update after transformation for studentId=$studentId")
+ logger.warn(s"[UserMappingStreamFunction] No profile data to update after transformation for studentId=$studentId")
+ metrics.incCounter(config.skipCount)
+ return
+ }
+
+ // Call User Service API to patch the profile
+ println(s"[UserMappingStreamFunction] Calling UserApiClient.patchProfile for studentId=$studentId...")
+ UserApiClient.patchProfile(studentId, profileData) match {
+ case Success(true) =>
+ println(s"[UserMappingStreamFunction] SUCCESS: Profile updated for studentId=$studentId")
+ logger.info(s"[UserMappingStreamFunction] Successfully updated profile for studentId=$studentId")
+ metrics.incCounter(config.successCount)
+
+ case Failure(exception) =>
+ println(s"[UserMappingStreamFunction] FAILED: Could not update profile for studentId=$studentId: ${exception.getMessage}")
+ logger.error(s"[UserMappingStreamFunction] Failed to update profile for studentId=$studentId", exception)
+ metrics.incCounter(config.skipCount)
+ // Re-throw to trigger Flink retry mechanism if configured
+ throw new Exception(s"Failed to update profile for studentId=$studentId", exception)
+ }
+
+ println(s"***************** Completed Processing Observation Event for studentId=$studentId *****************")
+
+ } catch {
+ case e: Exception =>
+ println(s"[UserMappingStreamFunction] EXCEPTION: Error processing observation event: ${e.getMessage}")
+ logger.error(s"[UserMappingStreamFunction] Exception processing observation event", e)
+ e.printStackTrace()
+ metrics.incCounter(config.skipCount)
+ // Re-throw to trigger Flink retry mechanism if configured
+ throw e
+ }
+ }
+
+}
diff --git a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/task/UserMappingStreamConfig.scala b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/task/UserMappingStreamConfig.scala
new file mode 100644
index 00000000..2d2ed2db
--- /dev/null
+++ b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/task/UserMappingStreamConfig.scala
@@ -0,0 +1,145 @@
+package org.shikshalokam.job.user.mapping.stream.processor.task
+
+import com.typesafe.config.Config
+import org.apache.flink.api.common.typeinfo.TypeInformation
+import org.apache.flink.api.java.typeutils.TypeExtractor
+import org.apache.flink.streaming.api.scala.OutputTag
+import org.shikshalokam.job.BaseJobConfig
+import org.shikshalokam.job.user.mapping.stream.processor.domain.Event
+
+
+class UserMappingStreamConfig(override val config: Config) extends BaseJobConfig(config, "UsersMappingStreamJob") {
+
+ implicit val mapTypeInfo: TypeInformation[Event] = TypeExtractor.getForClass(classOf[Event])
+
+ // Kafka Topics Configuration
+ val inputTopic: String = config.getString("kafka.input.topic")
+ val outputTopic: String = config.getString("kafka.output.topic")
+ val mentoringOutputTopic: String = config.getString("kafka.output.mentoring.topic")
+
+ // Output Tags
+ val eventOutputTag: OutputTag[String] = OutputTag[String]("user-dashboard-output-event")
+ val mentoringEventOutputTag: OutputTag[String] = OutputTag[String]("user-mentoring-output-event")
+
+ // Parallelism
+ override val kafkaConsumerParallelism: Int = config.getInt("task.consumer.parallelism")
+ val usersStreamParallelism: Int = config.getInt("task.sl.users.stream.parallelism")
+ val metabaseDashboardParallelism: Int = config.getInt("task.sl.metabase.dashboard.parallelism")
+
+ // Consumers
+ val usersStreamConsumer: String = "user-stream-consumer"
+ val metabaseDashboardProducer: String = "metabase-users-dashboard-producer"
+ val mentoringDashboardProducer: String = "metabase-mentoring-dashboard-producer"
+
+ // Functions
+ val usersStreamFunction: String = "UserMappingStreamFunction"
+
+ // user submissions job metrics
+ val usersCleanupHit: String = "user-cleanup-hit"
+ val skipCount: String = "skipped-message-count"
+ val successCount: String = "success-message-count"
+ val totalEventsCount: String = "total-user-events-count"
+
+
+ // PostgreSQL connection config
+ val pgHost: String = config.getString("postgres.host")
+ val pgPort: String = config.getString("postgres.port")
+ val pgUsername: String = config.getString("postgres.username")
+ val pgPassword: String = config.getString("postgres.password")
+ val pgDataBase: String = config.getString("postgres.database")
+ val userMetrics: String = config.getString("postgres.tables.userMetrics")
+ val dashboardMetadata: String = config.getString("postgres.tables.dashboardMetadataTable")
+
+ val createTenantUserMetadataTable: String =
+ s"""
+ |CREATE TABLE IF NOT EXISTS @tenantTable (
+ | id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ | user_id INT,
+ | attribute_code TEXT,
+ | attribute_value TEXT,
+ | attribute_label TEXT,
+ | UNIQUE (user_id, attribute_value)
+ |);
+ """.stripMargin
+
+ val createOrgRolesTable: String =
+ s"""
+ |CREATE TABLE IF NOT EXISTS @orgRolesTable (
+ | id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ | user_id INT,
+ | org_id INT,
+ | org_name TEXT,
+ | role_id INT,
+ | role_name TEXT,
+ | UNIQUE (user_id, org_id, role_id)
+ |);
+ """.stripMargin
+
+ val createTenantUserTable: String =
+ s"""
+ |CREATE TABLE IF NOT EXISTS @usersTable (
+ | id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ | user_id INT UNIQUE,
+ | tenant_code TEXT,
+ | username TEXT,
+ | name TEXT,
+ | status TEXT,
+ | is_deleted BOOLEAN,
+ | created_by INT,
+ | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ | user_profile_one_id TEXT,
+ | user_profile_one_name TEXT,
+ | user_profile_one_external_id TEXT,
+ | user_profile_two_id TEXT,
+ | user_profile_two_name TEXT,
+ | user_profile_two_external_id TEXT,
+ | user_profile_three_id TEXT,
+ | user_profile_three_name TEXT,
+ | user_profile_three_external_id TEXT,
+ | user_profile_four_id TEXT,
+ | user_profile_four_name TEXT,
+ | user_profile_four_external_id TEXT,
+ | user_profile_five_id TEXT,
+ | user_profile_five_name TEXT,
+ | user_profile_five_external_id TEXT
+ |);
+ """.stripMargin
+
+ val createUserMetricsTable: String =
+ s"""
+ |CREATE TABLE IF NOT EXISTS $userMetrics (
+ | id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ | tenant_code TEXT UNIQUE,
+ | total_users INT,
+ | active_users INT,
+ | deleted_users INT,
+ | last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+ |);
+ """.stripMargin
+
+ val createDashboardMetadataTable: String =
+ s"""
+ |CREATE TABLE IF NOT EXISTS $dashboardMetadata (
+ | id SERIAL PRIMARY KEY,
+ | entity_type TEXT NOT NULL,
+ | entity_name TEXT NOT NULL,
+ | entity_id TEXT UNIQUE NOT NULL,
+ | report_type TEXT,
+ | is_rubrics Boolean,
+ | parent_name TEXT,
+ | linked_to TEXT,
+ | main_metadata JSON,
+ | mi_metadata JSON,
+ | comparison_metadata JSON,
+ | status TEXT,
+ | error_message TEXT,
+ | state_details_url_state TEXT,
+ | state_details_url_admin TEXT,
+ | district_details_url_district TEXT,
+ | district_details_url_state TEXT,
+ | district_details_url_admin TEXT
+ |);
+ """.stripMargin
+
+}
\ No newline at end of file
diff --git a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/task/UserMappingStreamTask.scala b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/task/UserMappingStreamTask.scala
new file mode 100644
index 00000000..c9b7af17
--- /dev/null
+++ b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/task/UserMappingStreamTask.scala
@@ -0,0 +1,60 @@
+package org.shikshalokam.job.user.mapping.stream.processor.task
+
+import com.typesafe.config.ConfigFactory
+import org.apache.flink.api.common.typeinfo.TypeInformation
+import org.apache.flink.api.java.typeutils.TypeExtractor
+import org.apache.flink.api.java.utils.ParameterTool
+import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
+import org.shikshalokam.job.connector.FlinkKafkaConnector
+import org.shikshalokam.job.user.mapping.stream.processor.domain.ObservationEvent
+import org.shikshalokam.job.user.mapping.stream.processor.functions.UserMappingStreamFunction
+import org.shikshalokam.job.util.FlinkUtil
+
+import java.io.File
+
+class UserMappingStreamTask(config: UserMappingStreamConfig, kafkaConnector: FlinkKafkaConnector){
+
+ private val serialVersionUID = -7729362727131516112L
+ def process(): Unit = {
+ implicit val env: StreamExecutionEnvironment = FlinkUtil.getExecutionContext(config)
+ implicit val eventTypeInfo: TypeInformation[ObservationEvent] = TypeExtractor.getForClass(classOf[ObservationEvent])
+ implicit val stringTypeInfo: TypeInformation[String] = TypeExtractor.getForClass(classOf[String])
+ val source = kafkaConnector.kafkaJobRequestSource[ObservationEvent](config.inputTopic)
+
+ val progressStream = env.addSource(source).name(config.usersStreamConsumer)
+ .uid(config.usersStreamConsumer).setParallelism(config.kafkaConsumerParallelism)
+ .rebalance
+ .process(new UserMappingStreamFunction(config))
+ .name(config.usersStreamFunction).uid(config.usersStreamFunction)
+ .setParallelism(config.usersStreamParallelism)
+
+ progressStream.getSideOutput(config.eventOutputTag)
+ .addSink(kafkaConnector.kafkaStringSink(config.outputTopic))
+ .name(config.metabaseDashboardProducer)
+ .uid(config.metabaseDashboardProducer)
+ .setParallelism(config.metabaseDashboardParallelism)
+
+ // sink for mentoring dashboard events (new)
+ progressStream.getSideOutput(config.mentoringEventOutputTag)
+ .addSink(kafkaConnector.kafkaStringSink(config.mentoringOutputTopic))
+ .name(config.mentoringDashboardProducer)
+ .uid(config.mentoringDashboardProducer)
+ .setParallelism(config.metabaseDashboardParallelism)
+
+ env.execute(config.jobName)
+ }
+}
+
+object UserMappingStreamTask {
+ def main(args: Array[String]): Unit = {
+ println("Starting up the User Mapping Stream Job")
+ val configFilePath = Option(ParameterTool.fromArgs(args).get("config.file.path"))
+ val config = configFilePath.map {
+ path => ConfigFactory.parseFile(new File(path)).resolve()
+ }.getOrElse(ConfigFactory.load("user-mapping-stream.conf").withFallback(ConfigFactory.systemEnvironment()))
+ val userMappingStreamConfig = new UserMappingStreamConfig(config)
+ val kafkaUtil = new FlinkKafkaConnector(userMappingStreamConfig)
+ val task = new UserMappingStreamTask(userMappingStreamConfig, kafkaUtil)
+ task.process()
+ }
+}
\ No newline at end of file
diff --git a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/util/FieldMapper.scala b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/util/FieldMapper.scala
new file mode 100644
index 00000000..504cd466
--- /dev/null
+++ b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/util/FieldMapper.scala
@@ -0,0 +1,156 @@
+package org.shikshalokam.job.user.mapping.stream.processor.util
+
+import com.typesafe.config.ConfigFactory
+import org.slf4j.LoggerFactory
+import ujson.{Js, Obj}
+
+import java.util
+import scala.collection.JavaConverters._
+import scala.util.{Failure, Success, Try}
+
+/**
+ * FieldMapper utility to transform observation data fields to user profile fields
+ * based on mappings defined in field-mappings.conf
+ *
+ * Example mapping:
+ * name -> profile.name
+ * about -> profile.about
+ */
+object FieldMapper {
+
+ private val logger = LoggerFactory.getLogger(FieldMapper.getClass)
+
+ // Load field mappings from config file
+ private val mappings: Map[String, String] = loadMappings()
+
+ println(s"[FieldMapper] Loaded ${mappings.size} field mappings")
+ mappings.foreach { case (source, target) =>
+ println(s"[FieldMapper] Mapping: $source -> $target")
+ }
+
+ /**
+ * Load field mappings from field-mappings.conf
+ * @return Map of source field -> target field path
+ */
+ private def loadMappings(): Map[String, String] = {
+ try {
+ // Use parseResources to load only our config file, avoiding conflicts with other configs
+ val config = ConfigFactory.parseResources("field-mappings.conf")
+ if (config.isEmpty) {
+ logger.warn("[FieldMapper] field-mappings.conf is empty or not found")
+ println("[FieldMapper] WARNING: field-mappings.conf is empty or not found")
+ return Map.empty[String, String]
+ }
+
+ val configMap = config.entrySet().asScala.map { entry =>
+ val key = entry.getKey
+ val value = config.getString(key)
+ (key, value)
+ }.toMap
+
+ println(s"[FieldMapper] Successfully loaded ${configMap.size} mappings from field-mappings.conf")
+ configMap
+ } catch {
+ case e: Exception =>
+ logger.error(s"[FieldMapper] Failed to load field-mappings.conf: ${e.getMessage}", e)
+ println(s"[FieldMapper] ERROR: Failed to load field-mappings.conf: ${e.getMessage}")
+ e.printStackTrace()
+ Map.empty[String, String]
+ }
+ }
+
+ /**
+ * Transform observation data to user profile patch format
+ *
+ * Input observationData example:
+ * {
+ * "name": "Carol Miranda Updated Two",
+ * "about": "admin Update",
+ * "dob": "22-12-1990"
+ * }
+ *
+ * Output profile format:
+ * {
+ * "profile": {
+ * "name": "Carol Miranda Updated One",
+ * "about": "admin Update",
+ * "dob": "22-12-1990"
+ * }
+ * }
+ *
+ * @param observationData Map containing observation field values
+ * @return JsObject representing the profile patch structure
+ */
+ def transform(observationData: util.Map[String, Any]): Js.Obj = {
+ try {
+ println(s"[FieldMapper] Starting transformation of observation data")
+ println(s"[FieldMapper] Input observationData keys: ${observationData.keySet().asScala.mkString(", ")}")
+
+ val profileObj = Obj()
+
+ // Iterate through each mapping
+ mappings.foreach { case (sourceField, targetPath) =>
+ try {
+ // Get value from observation data
+ val value = observationData.get(sourceField)
+
+ if (value != null) {
+ // Parse target path (e.g., "profile.phone" -> ["profile", "phone"])
+ val pathParts = targetPath.split("\\.")
+
+ if (pathParts.length >= 2) {
+ val rootKey = pathParts(0) // e.g., "profile"
+ val fieldKey = pathParts(1) // e.g., "phone"
+
+ // We expect all mappings to be under "profile", so rootKey should be "profile"
+ if (rootKey == "profile") {
+ // Set the field value directly in profile object
+ value match {
+ case s: String => profileObj.value(fieldKey) = s
+ case n: Number =>
+ // Convert Java Number to ujson numeric value
+ profileObj.value(fieldKey) = n.doubleValue()
+ case i: Int => profileObj.value(fieldKey) = i
+ case l: Long => profileObj.value(fieldKey) = l
+ case d: Double => profileObj.value(fieldKey) = d
+ case f: Float => profileObj.value(fieldKey) = f
+ case b: Boolean => profileObj.value(fieldKey) = b
+ case _ => profileObj.value(fieldKey) = value.toString
+ }
+
+ println(s"[FieldMapper] Mapped $sourceField -> $targetPath = $value")
+ } else {
+ println(s"[FieldMapper] WARNING: Root key '$rootKey' is not 'profile', skipping field $sourceField")
+ }
+ } else {
+ println(s"[FieldMapper] WARNING: Invalid target path format: $targetPath (expected format: 'profile.field')")
+ }
+ } else {
+ println(s"[FieldMapper] Source field '$sourceField' not found in observation data, skipping")
+ }
+ } catch {
+ case e: Exception =>
+ logger.error(s"[FieldMapper] Error mapping field $sourceField: ${e.getMessage}", e)
+ println(s"[FieldMapper] ERROR mapping field $sourceField: ${e.getMessage}")
+ }
+ }
+
+ // Return the result with "profile" as root
+ val result = Obj("profile" -> profileObj)
+ println(s"[FieldMapper] Transformation complete. Result: ${result.render()}")
+ result
+
+ } catch {
+ case e: Exception =>
+ logger.error(s"[FieldMapper] Error during transformation: ${e.getMessage}", e)
+ println(s"[FieldMapper] ERROR during transformation: ${e.getMessage}")
+ e.printStackTrace()
+ Obj("profile" -> Obj()) // Return empty profile on error
+ }
+ }
+
+ /**
+ * Get the mappings for debugging
+ */
+ def getMappings: Map[String, String] = mappings
+}
diff --git a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/util/UserApiClient.scala b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/util/UserApiClient.scala
new file mode 100644
index 00000000..5a858116
--- /dev/null
+++ b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/util/UserApiClient.scala
@@ -0,0 +1,124 @@
+package org.shikshalokam.job.user.mapping.stream.processor.util
+
+import org.slf4j.LoggerFactory
+import ujson.{Js, Obj}
+import requests._
+
+import scala.util.{Failure, Success, Try}
+
+/**
+ * HTTP client for making PATCH requests to User Service API
+ *
+ * Endpoint: PATCH http://localhost:7001/user/v1/user/update
+ * Header: X-auth-token: {token}
+ * Content-Type: application/json
+ */
+object UserApiClient {
+
+ private val logger = LoggerFactory.getLogger(UserApiClient.getClass)
+
+ // User Service Configuration
+ private val BASE_URL = "http://localhost:7001"
+ private val AUTH_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImlkIjozMDg4LCJuYW1lIjoiQ2Fyb2wgTWlyYW5kYSIsInNlc3Npb25faWQiOjIzMDE4LCJvcmdhbml6YXRpb25faWRzIjpbIjY3Il0sIm9yZ2FuaXphdGlvbl9jb2RlcyI6WyJicmFjX2dibCJdLCJ0ZW5hbnRfY29kZSI6ImJyYWMiLCJvcmdhbml6YXRpb25zIjpbeyJpZCI6NjcsIm5hbWUiOiJCUkFDIEdCTCBvcmciLCJjb2RlIjoiYnJhY19nYmwiLCJkZXNjcmlwdGlvbiI6IkJSQUMgR0JMIG9yZyIsInN0YXR1cyI6IkFDVElWRSIsInJlbGF0ZWRfb3JncyI6bnVsbCwidGVuYW50X2NvZGUiOiJicmFjIiwibWV0YSI6bnVsbCwiY3JlYXRlZF9ieSI6bnVsbCwidXBkYXRlZF9ieSI6MSwicm9sZXMiOlt7ImlkIjoyMTMsInRpdGxlIjoic2Vzc2lvbl9tYW5hZ2VyIiwibGFiZWwiOiJMaW5rYWdlIENoYW1waW9uIiwidXNlcl90eXBlIjowLCJzdGF0dXMiOiJBQ1RJVkUiLCJvcmdhbml6YXRpb25faWQiOjY3LCJ2aXNpYmlsaXR5IjoiUFVCTElDIiwidGVuYW50X2NvZGUiOiJicmFjIiwidHJhbnNsYXRpb25zIjpudWxsfV19XX0sImlhdCI6MTc2ODM4Mzc1MCwiZXhwIjoxNzY4NDcwMTUwfQ.T8Ycr0X3bbVOCi63p8CdHlt9hAwClrKS9euGwV6ht78"
+
+ println(s"[UserApiClient] Initialized with BASE_URL: $BASE_URL")
+ println(s"[UserApiClient] AUTH_TOKEN configured: ${if (AUTH_TOKEN.nonEmpty) "***" else "NOT SET"}")
+
+ /**
+ * Patch user profile data for a student
+ *
+ * @param studentId The student ID to update
+ * @param profileData JSON object containing profile data (e.g., {"profile": {"phone": "...", "gender": "..."}})
+ * @return Success(true) if update successful, Failure(exception) otherwise
+ */
+ def patchProfile(studentId: String, profileData: Js.Obj): Try[Boolean] = {
+ if (studentId == null || studentId.trim.isEmpty) {
+ val error = new IllegalArgumentException("studentId cannot be null or empty")
+ logger.error("[UserApiClient] studentId is null or empty", error)
+ println(s"[UserApiClient] ERROR: studentId is null or empty")
+ return Failure(error)
+ }
+
+ try {
+ val url = s"$BASE_URL/user/v1/user/update"
+
+ // Merge studentId with profileData into a single payload
+ // The API expects the payload directly, so we'll include studentId and merge profile fields
+ val payloadObj = Obj()
+
+ // Add studentId to payload to identify which user to update
+ payloadObj.value("id") = studentId
+
+ // Merge profile fields from profileData into payload
+ val profileObj = profileData.value.get("profile")
+ if (profileObj.isDefined) {
+ // Merge profile fields directly into payload (flatten the structure)
+ profileObj.get.asInstanceOf[Obj].value.foreach { case (key, value) =>
+ payloadObj.value(key) = value
+ }
+ } else {
+ // If no profile object, use the entire profileData
+ profileData.value.foreach { case (key, value) =>
+ payloadObj.value(key) = value
+ }
+ }
+
+ val jsonPayload = payloadObj.render()
+
+ println(s"[UserApiClient] PATCH Request to: $url")
+ println(s"[UserApiClient] Request payload: $jsonPayload")
+
+ val headers = Map(
+ "X-auth-token" -> AUTH_TOKEN,
+ "Content-Type" -> "application/json"
+ )
+
+ println(s"[UserApiClient] Request headers: X-auth-token=***, Content-Type=application/json")
+
+ val response = requests.patch(
+ url,
+ data = jsonPayload,
+ headers = headers
+ )
+
+ println(s"[UserApiClient] Response status code: ${response.statusCode}")
+ println(s"[UserApiClient] Response body: ${response.text}")
+
+ if (response.statusCode >= 200 && response.statusCode < 300) {
+ println(s"[UserApiClient] SUCCESS: Profile updated for studentId=$studentId")
+ logger.info(s"[UserApiClient] Successfully updated profile for studentId=$studentId")
+ Success(true)
+ } else {
+ val error = new Exception(s"User Service API returned status ${response.statusCode}: ${response.text}")
+ logger.error(s"[UserApiClient] API error for studentId=$studentId: ${error.getMessage}", error)
+ println(s"[UserApiClient] ERROR: API returned status ${response.statusCode}: ${response.text}")
+ Failure(error)
+ }
+
+ } catch {
+ case e: Exception =>
+ logger.error(s"[UserApiClient] Exception while patching profile for studentId=$studentId: ${e.getMessage}", e)
+ println(s"[UserApiClient] EXCEPTION: ${e.getMessage}")
+ e.printStackTrace()
+ Failure(e)
+ }
+ }
+
+ /**
+ * Test connection to user service (for debugging)
+ */
+ def testConnection(): Try[Boolean] = {
+ try {
+ val url = s"$BASE_URL/api/users/health"
+ println(s"[UserApiClient] Testing connection to: $url")
+
+ val response = requests.get(url)
+ println(s"[UserApiClient] Health check response: ${response.statusCode}")
+ Success(response.statusCode == 200)
+ } catch {
+ case e: Exception =>
+ println(s"[UserApiClient] Connection test failed: ${e.getMessage}")
+ Failure(e)
+ }
+ }
+}
diff --git a/stream-jobs/user-mapping-stream-processor/src/test/resources/test.conf b/stream-jobs/user-mapping-stream-processor/src/test/resources/test.conf
new file mode 100644
index 00000000..531ea25c
--- /dev/null
+++ b/stream-jobs/user-mapping-stream-processor/src/test/resources/test.conf
@@ -0,0 +1,26 @@
+include "base-test.conf"
+
+kafka {
+ input.topic = "dev.userCreate"
+ groupId = "dev.users"
+ output.topic = "sl-metabase-user-dashboard-dev"
+ output.mentoring.topic = "sl-metabase-mentoring-dashboard-dev"
+}
+
+task {
+ consumer.parallelism = 1
+ sl.users.stream.parallelism = 1
+ sl.metabase.dashboard.parallelism = 1
+}
+
+postgres{
+ host = "localhost"
+ port = "5432"
+ username = "postgres"
+ password = "Test@123"
+ database = "test"
+ tables = {
+ userMetrics = ${job.env}"_user_metrics"
+ dashboardMetadataTable = ${job.env}"_dashboard_metadata"
+ }
+}
diff --git a/stream-jobs/user-mapping-stream-processor/src/test/scala/org/shikshalokam/user/mapping/stream/processor/fixture/ObservationEventsMock.scala b/stream-jobs/user-mapping-stream-processor/src/test/scala/org/shikshalokam/user/mapping/stream/processor/fixture/ObservationEventsMock.scala
new file mode 100644
index 00000000..6c88c01a
--- /dev/null
+++ b/stream-jobs/user-mapping-stream-processor/src/test/scala/org/shikshalokam/user/mapping/stream/processor/fixture/ObservationEventsMock.scala
@@ -0,0 +1,37 @@
+package org.shikshalokam.user.mapping.stream.processor.fixture
+
+object ObservationEventsMock {
+
+ // Sample observation submission event matching the expected format
+ val OBSERVATION_SUBMITTED: String = """{
+ "eventType": "observation-submitted",
+ "id": 3088,
+ "organizationId": 1,
+ "observationData": {
+ "name": "Carol Miranda Updated Two",
+ "about": "admin Update",
+ "dob": "22-12-1990"
+ }
+ }"""
+
+ // Additional test case with minimal data (only name)
+ val OBSERVATION_SUBMITTED_MINIMAL: String = """{
+ "eventType": "observation-submitted",
+ "id": 3088,
+ "organizationId": 2,
+ "observationData": {
+ "name": "Test Student Name"
+ }
+ }"""
+
+ // Test case with only about field
+ val OBSERVATION_SUBMITTED_ABOUT_ONLY: String = """{
+ "eventType": "observation-submitted",
+ "id": 3089,
+ "organizationId": 3,
+ "observationData": {
+ "about": "Updated about information"
+ }
+ }"""
+
+}
diff --git a/stream-jobs/user-mapping-stream-processor/src/test/scala/org/shikshalokam/user/mapping/stream/processor/spec/GenerateUserSink.scala b/stream-jobs/user-mapping-stream-processor/src/test/scala/org/shikshalokam/user/mapping/stream/processor/spec/GenerateUserSink.scala
new file mode 100644
index 00000000..d4d463b7
--- /dev/null
+++ b/stream-jobs/user-mapping-stream-processor/src/test/scala/org/shikshalokam/user/mapping/stream/processor/spec/GenerateUserSink.scala
@@ -0,0 +1,19 @@
+package org.shikshalokam.user.mapping.stream.processor.spec
+
+import org.apache.flink.streaming.api.functions.sink.SinkFunction
+
+import java.util
+
+class GenerateUserSink extends SinkFunction[String] {
+
+ override def invoke(value: String): Unit = {
+ synchronized{
+ println(value)
+ GenerateUserSink.values.add(value)
+ }
+ }
+}
+
+object GenerateUserSink {
+ val values: util.List[String] = new util.ArrayList()
+}
\ No newline at end of file
diff --git a/stream-jobs/user-mapping-stream-processor/src/test/scala/org/shikshalokam/user/mapping/stream/processor/spec/ObservationEventSource.scala b/stream-jobs/user-mapping-stream-processor/src/test/scala/org/shikshalokam/user/mapping/stream/processor/spec/ObservationEventSource.scala
new file mode 100644
index 00000000..84daaa2b
--- /dev/null
+++ b/stream-jobs/user-mapping-stream-processor/src/test/scala/org/shikshalokam/user/mapping/stream/processor/spec/ObservationEventSource.scala
@@ -0,0 +1,17 @@
+package org.shikshalokam.user.mapping.stream.processor.spec
+
+import org.apache.flink.streaming.api.functions.source.SourceFunction
+import org.apache.flink.streaming.api.functions.source.SourceFunction.SourceContext
+import org.shikshalokam.job.user.mapping.stream.processor.domain.ObservationEvent
+import org.shikshalokam.job.util.JSONUtil
+import org.shikshalokam.user.mapping.stream.processor.fixture.ObservationEventsMock
+
+class ObservationEventSource extends SourceFunction[ObservationEvent] {
+
+ override def run(ctx: SourceContext[ObservationEvent]): Unit = {
+ ctx.collect(new ObservationEvent(JSONUtil.deserialize[java.util.Map[String, Any]](ObservationEventsMock.OBSERVATION_SUBMITTED), 0, 0))
+ }
+
+ override def cancel(): Unit = {}
+
+}
diff --git a/stream-jobs/user-mapping-stream-processor/src/test/scala/org/shikshalokam/user/mapping/stream/processor/spec/UserMappingStreamFunctionTestSpec.scala b/stream-jobs/user-mapping-stream-processor/src/test/scala/org/shikshalokam/user/mapping/stream/processor/spec/UserMappingStreamFunctionTestSpec.scala
new file mode 100644
index 00000000..0ffff4a2
--- /dev/null
+++ b/stream-jobs/user-mapping-stream-processor/src/test/scala/org/shikshalokam/user/mapping/stream/processor/spec/UserMappingStreamFunctionTestSpec.scala
@@ -0,0 +1,59 @@
+package org.shikshalokam.user.mapping.stream.processor.spec
+
+import com.typesafe.config.{Config, ConfigFactory}
+import org.apache.flink.api.common.typeinfo.TypeInformation
+import org.apache.flink.api.java.typeutils.TypeExtractor
+import org.apache.flink.runtime.testutils.MiniClusterResourceConfiguration
+import org.apache.flink.test.util.MiniClusterWithClientResource
+import org.mockito.Mockito
+import org.mockito.Mockito.when
+import org.shikshalokam.BaseTestSpec
+import org.shikshalokam.job.connector.FlinkKafkaConnector
+import org.shikshalokam.job.user.mapping.stream.processor.domain.ObservationEvent
+import org.shikshalokam.job.user.mapping.stream.processor.task.{UserMappingStreamConfig, UserMappingStreamTask}
+
+
+class UserMappingStreamFunctionTestSpec extends BaseTestSpec {
+
+ implicit val mapTypeInfo: TypeInformation[java.util.Map[String, AnyRef]] = TypeExtractor.getForClass(classOf[java.util.Map[String, AnyRef]])
+ implicit val eventTypeInfo: TypeInformation[ObservationEvent] = TypeExtractor.getForClass(classOf[ObservationEvent])
+ implicit val stringTypeInfo: TypeInformation[String] = TypeExtractor.getForClass(classOf[String])
+
+ val flinkCluster = new MiniClusterWithClientResource(new MiniClusterResourceConfiguration.Builder()
+ .setConfiguration(testConfiguration())
+ .setNumberSlotsPerTaskManager(1)
+ .setNumberTaskManagers(1)
+ .build)
+
+ val mockKafkaUtil: FlinkKafkaConnector = mock[FlinkKafkaConnector](Mockito.withSettings().serializable())
+
+ val config: Config = ConfigFactory.load("test.conf")
+ val jobConfig: UserMappingStreamConfig = new UserMappingStreamConfig(config)
+
+
+ override protected def beforeAll(): Unit = {
+ super.beforeAll()
+ //Embedded Postgres connection
+ flinkCluster.before()
+ }
+
+ override protected def afterAll(): Unit = {
+ super.afterAll()
+ flinkCluster.after()
+ }
+
+ def initialize(): Unit = {
+ when(mockKafkaUtil.kafkaJobRequestSource[ObservationEvent](jobConfig.inputTopic))
+ .thenReturn(new ObservationEventSource)
+ when(mockKafkaUtil.kafkaStringSink(jobConfig.outputTopic))
+ .thenReturn(new GenerateUserSink)
+ when(mockKafkaUtil.kafkaStringSink(jobConfig.mentoringOutputTopic))
+ .thenReturn(new GenerateUserSink)
+ }
+
+ "Observation Mapping Stream Job " should "execute successfully " in {
+ initialize()
+ new UserMappingStreamTask(jobConfig, mockKafkaUtil).process()
+ }
+
+}
\ No newline at end of file
From ba21eaa78de4b0c3417aca42ce0af2f3d3b11729 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E2=80=9CKomal?= <“komal_m@tekditechnologies.com”>
Date: Thu, 22 Jan 2026 17:11:20 +0530
Subject: [PATCH 02/22] Task #252259 feat: When the household profile
Observation is submitted, participant's profile should be updated
---
.../obs_kafka_response.json | 6772 +++++++++++++++++
.../src/main/resources/field-mappings.conf | 7 +-
.../main/resources/user-mapping-stream.conf | 8 +-
.../processor/domain/ObservationEvent.scala | 19 +-
.../functions/UserMappingStreamFunction.scala | 5 +
.../stream/processor/util/FieldMapper.scala | 31 +-
.../stream/processor/util/UserApiClient.scala | 2 +-
.../spec/ObservationEventSource.scala | 6 +-
8 files changed, 6830 insertions(+), 20 deletions(-)
create mode 100644 stream-jobs/user-mapping-stream-processor/obs_kafka_response.json
diff --git a/stream-jobs/user-mapping-stream-processor/obs_kafka_response.json b/stream-jobs/user-mapping-stream-processor/obs_kafka_response.json
new file mode 100644
index 00000000..d4e30c6d
--- /dev/null
+++ b/stream-jobs/user-mapping-stream-processor/obs_kafka_response.json
@@ -0,0 +1,6772 @@
+{
+ "_id": "696f0469add4262f415219ad",
+ "entityId": "6953d07fe83c1c0014713c05",
+ "observationId": "696f0469add4262f415219a0",
+ "createdBy": "3087",
+ "status": "completed",
+ "evidencesStatus": [
+ {
+ "externalId": "OB",
+ "tip": null,
+ "name": "Observation",
+ "description": null,
+ "modeOfCollection": "onfield",
+ "canBeNotApplicable": false,
+ "notApplicable": null,
+ "canBeNotAllowed": false,
+ "remarks": null,
+ "startTime": 1768894219120,
+ "endTime": 1768894222118,
+ "isSubmitted": true,
+ "submissions": [
+ {
+ "externalId": "OB",
+ "startTime": 1768894219120,
+ "endTime": 1768894222118,
+ "isSubmitted": false,
+ "gpsLocation": null,
+ "submittedBy": "3087",
+ "submittedByName": "Farabi Ahmedullah",
+ "submittedByEmail": null,
+ "submissionDate": "2026-01-20T07:30:24.523Z",
+ "isValid": true
+ }
+ ],
+ "hasConflicts": false
+ }
+ ],
+ "evidences": {
+ "OB": {
+ "externalId": "OB",
+ "tip": null,
+ "name": "Observation",
+ "description": null,
+ "modeOfCollection": "onfield",
+ "canBeNotApplicable": false,
+ "notApplicable": false,
+ "canBeNotAllowed": false,
+ "remarks": null,
+ "startTime": 1768894219120,
+ "endTime": 1768894222118,
+ "isSubmitted": true,
+ "submissions": [
+ {
+ "status": "submit",
+ "externalId": "OB",
+ "answers": {
+ "696660cdf56b708ddc8dfbb2": {
+ "qid": "696660cdf56b708ddc8dfbb2",
+ "value": "sagar",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Facilitator Name",
+ ""
+ ],
+ "labels": [
+ "sagar"
+ ],
+ "responseType": "text",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135767,
+ "endTime": 1768892119361,
+ "criteriaId": "696660bde56b708ddc8dfc55",
+ "responseType": "text",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bce56b708ddc8dfbb2": {
+ "qid": "696660bce56b708ddc8dfbb2",
+ "value": "good",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Province",
+ ""
+ ],
+ "labels": [
+ "good"
+ ],
+ "responseType": "text",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135789,
+ "endTime": 1768892124079,
+ "criteriaId": "696660bde56b708ddc8dfc55",
+ "responseType": "text",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bce56b708ddc8dfbb3": {
+ "qid": "696660bce56b708ddc8dfbb3",
+ "value": "20",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Pilot Site",
+ ""
+ ],
+ "labels": [
+ "20"
+ ],
+ "responseType": "text",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135796,
+ "endTime": 1768892127781,
+ "criteriaId": "696660bde56b708ddc8dfc55",
+ "responseType": "text",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bce56b708ddc8dfbb4": {
+ "qid": "696660bce56b708ddc8dfbb4",
+ "value": "2026-01-20T06:55:00.000Z",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Date of Collection",
+ ""
+ ],
+ "labels": [
+ "2026-01-20T06:55:00.000Z"
+ ],
+ "responseType": "date",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135803,
+ "endTime": 1768892129231,
+ "criteriaId": "696660bde56b708ddc8dfc55",
+ "responseType": "date",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bce56b708ddc8dfbb5": {
+ "qid": "696660bce56b708ddc8dfbb5",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Record if the captured residential address is correct",
+ ""
+ ],
+ "labels": [
+ "Yes"
+ ],
+ "responseType": "radio",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135820,
+ "endTime": 1768892136961,
+ "criteriaId": "696660bde56b708ddc8dfc57",
+ "responseType": "radio",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bce56b708ddc8dfbb6": {
+ "qid": "696660bce56b708ddc8dfbb6",
+ "value": "vaibhav",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "What is your name?",
+ ""
+ ],
+ "labels": [
+ "vaibhav"
+ ],
+ "responseType": "text",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135827,
+ "endTime": 1768892300964,
+ "criteriaId": "696660bde56b708ddc8dfc57",
+ "responseType": "text",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bce56b708ddc8dfbb7": {
+ "qid": "696660bce56b708ddc8dfbb7",
+ "value": "123",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "What is your ID number?",
+ ""
+ ],
+ "labels": [
+ "123"
+ ],
+ "responseType": "text",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135845,
+ "endTime": 1768892306434,
+ "criteriaId": "696660bde56b708ddc8dfc57",
+ "responseType": "text",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bce56b708ddc8dfbb8": {
+ "qid": "696660bce56b708ddc8dfbb8",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Is the respondent a man or a woman? (record from observation)",
+ ""
+ ],
+ "labels": [
+ "Man (Male)"
+ ],
+ "responseType": "radio",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135850,
+ "endTime": 1768892306546,
+ "criteriaId": "696660bde56b708ddc8dfc57",
+ "responseType": "radio",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bce56b708ddc8dfbb9": {
+ "qid": "696660bce56b708ddc8dfbb9",
+ "value": "9876543210",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "What is your cell phone number?",
+ ""
+ ],
+ "labels": [
+ "9876543210"
+ ],
+ "responseType": "text",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135855,
+ "endTime": 1768892315705,
+ "criteriaId": "696660bde56b708ddc8dfc57",
+ "responseType": "text",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bce56b708ddc8dfbba": {
+ "qid": "696660bce56b708ddc8dfbba",
+ "value": "9087654321",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Please may I have an alternative number for you?",
+ ""
+ ],
+ "labels": [
+ "9087654321"
+ ],
+ "responseType": "text",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135859,
+ "endTime": 1768892325383,
+ "criteriaId": "696660bde56b708ddc8dfc57",
+ "responseType": "text",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bce56b708ddc8dfbbb": {
+ "qid": "696660bce56b708ddc8dfbbb",
+ "value": "sagar@gmail.com",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "And what is your email address?",
+ ""
+ ],
+ "labels": [
+ "sagar@gmail.com"
+ ],
+ "responseType": "text",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135865,
+ "endTime": 1768892330800,
+ "criteriaId": "696660bde56b708ddc8dfc57",
+ "responseType": "text",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bce56b708ddc8dfbbc": {
+ "qid": "696660bce56b708ddc8dfbbc",
+ "value": "R2",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Does the respondent have any disabilities?",
+ ""
+ ],
+ "labels": [
+ "No"
+ ],
+ "responseType": "radio",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135868,
+ "endTime": 1768892332857,
+ "criteriaId": "696660bde56b708ddc8dfc57",
+ "responseType": "radio",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bce56b708ddc8dfbbd": {
+ "qid": "696660bce56b708ddc8dfbbd",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Which disability?",
+ ""
+ ],
+ "responseType": "text",
+ "filesNotUploaded": []
+ },
+ "startTime": "",
+ "endTime": "",
+ "criteriaId": "696660bde56b708ddc8dfc57",
+ "responseType": "text",
+ "evidenceMethod": "OB",
+ "visibleIf": [
+ {
+ "operator": "===",
+ "value": "R1",
+ "_id": "696660bce56b708ddc8dfbbc"
+ }
+ ],
+ "rubricLevel": ""
+ },
+ "696660bce56b708ddc8dfbbe": {
+ "qid": "696660bce56b708ddc8dfbbe",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Who is the head of this household?",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135873,
+ "endTime": 1768892334962,
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bce56b708ddc8dfbbf": {
+ "qid": "696660bce56b708ddc8dfbbf",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Please specify:",
+ ""
+ ],
+ "responseType": "text",
+ "filesNotUploaded": []
+ },
+ "startTime": "",
+ "endTime": "",
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "responseType": "text",
+ "evidenceMethod": "OB",
+ "visibleIf": [
+ {
+ "operator": "===",
+ "value": "R7",
+ "_id": "696660bce56b708ddc8dfbbe"
+ }
+ ],
+ "rubricLevel": ""
+ },
+ "696660bce56b708ddc8dfbc0": {
+ "qid": "696660bce56b708ddc8dfbc0",
+ "value": "R2",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Do you have children under the age of 18 living with you here in this household?",
+ ""
+ ],
+ "labels": [
+ "No"
+ ],
+ "responseType": "radio",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135884,
+ "endTime": 1768892338963,
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "responseType": "radio",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bce56b708ddc8dfbc1": {
+ "qid": "696660bce56b708ddc8dfbc1",
+ "value": [],
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Children Information",
+ ""
+ ],
+ "labels": [],
+ "responseType": "matrix",
+ "filesNotUploaded": []
+ },
+ "startTime": "",
+ "endTime": "",
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "responseType": "matrix",
+ "evidenceMethod": "OB",
+ "visibleIf": [
+ {
+ "operator": "===",
+ "value": "R1",
+ "_id": "696660bce56b708ddc8dfbc0"
+ }
+ ],
+ "rubricLevel": "",
+ "countOfInstances": 0
+ },
+ "696660bce56b708ddc8dfbc6": {
+ "qid": "696660bce56b708ddc8dfbc6",
+ "value": 19,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "How many other people over the age of 18 are living in the household?",
+ ""
+ ],
+ "labels": [
+ 19
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135887,
+ "endTime": 1768892345891,
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bce56b708ddc8dfbc7": {
+ "qid": "696660bce56b708ddc8dfbc7",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "18-24 years",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135893,
+ "endTime": 1768892356170,
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bce56b708ddc8dfbc8": {
+ "qid": "696660bce56b708ddc8dfbc8",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "25-34 years",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135897,
+ "endTime": 1768892357034,
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bce56b708ddc8dfbc9": {
+ "qid": "696660bce56b708ddc8dfbc9",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "35-39 years",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135910,
+ "endTime": 1768892357822,
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bce56b708ddc8dfbca": {
+ "qid": "696660bce56b708ddc8dfbca",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "60 or older",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135914,
+ "endTime": 1768892359002,
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bce56b708ddc8dfbcb": {
+ "qid": "696660bce56b708ddc8dfbcb",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Never been to school",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135917,
+ "endTime": 1768892360178,
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bce56b708ddc8dfbcc": {
+ "qid": "696660bce56b708ddc8dfbcc",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Crèche/nursery school",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135921,
+ "endTime": 1768892360528,
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bce56b708ddc8dfbcd": {
+ "qid": "696660bce56b708ddc8dfbcd",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Some primary school",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135924,
+ "endTime": 1768892360787,
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bce56b708ddc8dfbce": {
+ "qid": "696660bce56b708ddc8dfbce",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Primary completed",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135927,
+ "endTime": 1768892361132,
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bce56b708ddc8dfbcf": {
+ "qid": "696660bce56b708ddc8dfbcf",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Some high school",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135929,
+ "endTime": 1768892361495,
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bce56b708ddc8dfbd0": {
+ "qid": "696660bce56b708ddc8dfbd0",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "High school completed (Matric)",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135932,
+ "endTime": 1768892362105,
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bce56b708ddc8dfbd1": {
+ "qid": "696660bce56b708ddc8dfbd1",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "N. Qualification/Vocational training",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135935,
+ "endTime": 1768892362541,
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bce56b708ddc8dfbd2": {
+ "qid": "696660bce56b708ddc8dfbd2",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Higher certificate",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135954,
+ "endTime": 1768892362828,
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bce56b708ddc8dfbd3": {
+ "qid": "696660bce56b708ddc8dfbd3",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Diploma",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135959,
+ "endTime": 1768892363139,
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bce56b708ddc8dfbd4": {
+ "qid": "696660bce56b708ddc8dfbd4",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "University degree",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135967,
+ "endTime": 1768892363487,
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bce56b708ddc8dfbd5": {
+ "qid": "696660bce56b708ddc8dfbd5",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "How many of each of these grants does your household receive?",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135974,
+ "endTime": 1768892231025,
+ "criteriaId": "696660bde56b708ddc8dfc5b",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bce56b708ddc8dfbd6": {
+ "qid": "696660bce56b708ddc8dfbd6",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Child support grant",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135977,
+ "endTime": 1768892231983,
+ "criteriaId": "696660bde56b708ddc8dfc5b",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bce56b708ddc8dfbd7": {
+ "qid": "696660bce56b708ddc8dfbd7",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Child support grant top up",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135980,
+ "endTime": 1768892232653,
+ "criteriaId": "696660bde56b708ddc8dfc5b",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bce56b708ddc8dfbd8": {
+ "qid": "696660bce56b708ddc8dfbd8",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Foster care grant",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135984,
+ "endTime": 1768892233147,
+ "criteriaId": "696660bde56b708ddc8dfc5b",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bce56b708ddc8dfbd9": {
+ "qid": "696660bce56b708ddc8dfbd9",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Government disability grant",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135989,
+ "endTime": 1768892233558,
+ "criteriaId": "696660bde56b708ddc8dfc5b",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbda": {
+ "qid": "696660bde56b708ddc8dfbda",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "War veterans grant",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135993,
+ "endTime": 1768892234216,
+ "criteriaId": "696660bde56b708ddc8dfc5b",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbdb": {
+ "qid": "696660bde56b708ddc8dfbdb",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Covid-19 social grant",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135996,
+ "endTime": 1768892235268,
+ "criteriaId": "696660bde56b708ddc8dfc5b",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbdc": {
+ "qid": "696660bde56b708ddc8dfbdc",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Social Relief of Distress (SRD) grant",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135999,
+ "endTime": 1768892235815,
+ "criteriaId": "696660bde56b708ddc8dfc5b",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbdd": {
+ "qid": "696660bde56b708ddc8dfbdd",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Grant for older persons",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136001,
+ "endTime": 1768892236165,
+ "criteriaId": "696660bde56b708ddc8dfc5b",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbde": {
+ "qid": "696660bde56b708ddc8dfbde",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Care dependency grant",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136004,
+ "endTime": 1768892236497,
+ "criteriaId": "696660bde56b708ddc8dfc5b",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbdf": {
+ "qid": "696660bde56b708ddc8dfbdf",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Grant-in-Aid",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136007,
+ "endTime": 1768892236827,
+ "criteriaId": "696660bde56b708ddc8dfc5b",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbe0": {
+ "qid": "696660bde56b708ddc8dfbe0",
+ "value": [
+ "R2"
+ ],
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Now, which of these grants do YOU receive?",
+ ""
+ ],
+ "labels": [
+ "R2"
+ ],
+ "responseType": "multiselect-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136009,
+ "endTime": 1768892240771,
+ "criteriaId": "696660bde56b708ddc8dfc5b",
+ "responseType": "multiselect-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbe1": {
+ "qid": "696660bde56b708ddc8dfbe1",
+ "value": "R2",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Unemployment insurance fund/UIF/UIF TERS Covid Benefits",
+ ""
+ ],
+ "labels": [
+ "R2"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136021,
+ "endTime": 1768892243834,
+ "criteriaId": "696660bde56b708ddc8dfc5d",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbe2": {
+ "qid": "696660bde56b708ddc8dfbe2",
+ "value": "R2",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Income from household business",
+ ""
+ ],
+ "labels": [
+ "R2"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136027,
+ "endTime": 1768892246041,
+ "criteriaId": "696660bde56b708ddc8dfc5d",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbe3": {
+ "qid": "696660bde56b708ddc8dfbe3",
+ "value": "R2",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Salary/wages from PUBLIC SECTOR job",
+ ""
+ ],
+ "labels": [
+ "R2"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136033,
+ "endTime": 1768892247941,
+ "criteriaId": "696660bde56b708ddc8dfc5d",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbe4": {
+ "qid": "696660bde56b708ddc8dfbe4",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Salary/wages from PRIVATE SECTOR job",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136041,
+ "endTime": 1768892249559,
+ "criteriaId": "696660bde56b708ddc8dfc5d",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbe5": {
+ "qid": "696660bde56b708ddc8dfbe5",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Salary/wage from individuals (domestic worker, gardener)",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136051,
+ "endTime": 1768892250456,
+ "criteriaId": "696660bde56b708ddc8dfc5d",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbe6": {
+ "qid": "696660bde56b708ddc8dfbe6",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Money from rent you receive",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136063,
+ "endTime": 1768892251059,
+ "criteriaId": "696660bde56b708ddc8dfc5d",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbe7": {
+ "qid": "696660bde56b708ddc8dfbe7",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Piece job (fixed piece rate for task)",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136071,
+ "endTime": 1768892251580,
+ "criteriaId": "696660bde56b708ddc8dfc5d",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbe8": {
+ "qid": "696660bde56b708ddc8dfbe8",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Work pension or provident fund",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136078,
+ "endTime": 1768892252293,
+ "criteriaId": "696660bde56b708ddc8dfc5d",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbe9": {
+ "qid": "696660bde56b708ddc8dfbe9",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Money from friend or family member",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136087,
+ "endTime": 1768892254554,
+ "criteriaId": "696660bde56b708ddc8dfc5d",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbea": {
+ "qid": "696660bde56b708ddc8dfbea",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Money from maintenance from former spouse/partner",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136094,
+ "endTime": 1768892255359,
+ "criteriaId": "696660bde56b708ddc8dfc5d",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbeb": {
+ "qid": "696660bde56b708ddc8dfbeb",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Other income source",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136102,
+ "endTime": 1768892255927,
+ "criteriaId": "696660bde56b708ddc8dfc5d",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbec": {
+ "qid": "696660bde56b708ddc8dfbec",
+ "value": "hello",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Please specify other income source",
+ ""
+ ],
+ "labels": [
+ "hello"
+ ],
+ "responseType": "text",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136111,
+ "endTime": 1768892261589,
+ "criteriaId": "696660bde56b708ddc8dfc5d",
+ "responseType": "text",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbed": {
+ "qid": "696660bde56b708ddc8dfbed",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Unemployment insurance fund/UIF/UIF TERS Covid Benefits",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136116,
+ "endTime": 1768892262794,
+ "criteriaId": "696660bde56b708ddc8dfc5f",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbee": {
+ "qid": "696660bde56b708ddc8dfbee",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Income from my own business",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136126,
+ "endTime": 1768892263361,
+ "criteriaId": "696660bde56b708ddc8dfc5f",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbef": {
+ "qid": "696660bde56b708ddc8dfbef",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Salary/wages from PUBLIC SECTOR job",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136138,
+ "endTime": 1768892263920,
+ "criteriaId": "696660bde56b708ddc8dfc5f",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbf0": {
+ "qid": "696660bde56b708ddc8dfbf0",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Salary/wages from PRIVATE SECTOR job",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136146,
+ "endTime": 1768892264477,
+ "criteriaId": "696660bde56b708ddc8dfc5f",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbf1": {
+ "qid": "696660bde56b708ddc8dfbf1",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Salary/wage from individuals (domestic worker, gardener)",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136152,
+ "endTime": 1768892264949,
+ "criteriaId": "696660bde56b708ddc8dfc5f",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbf2": {
+ "qid": "696660bde56b708ddc8dfbf2",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Money from rent you receive",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136159,
+ "endTime": 1768892265483,
+ "criteriaId": "696660bde56b708ddc8dfc5f",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbf3": {
+ "qid": "696660bde56b708ddc8dfbf3",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Piece job (fixed piece rate for task)",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136169,
+ "endTime": 1768892266078,
+ "criteriaId": "696660bde56b708ddc8dfc5f",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbf4": {
+ "qid": "696660bde56b708ddc8dfbf4",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Work pension or provident fund",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136177,
+ "endTime": 1768892266577,
+ "criteriaId": "696660bde56b708ddc8dfc5f",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbf5": {
+ "qid": "696660bde56b708ddc8dfbf5",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Money from friend or family member",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136185,
+ "endTime": 1768892267094,
+ "criteriaId": "696660bde56b708ddc8dfc5f",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbf6": {
+ "qid": "696660bde56b708ddc8dfbf6",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Money from maintenance from former spouse/partner",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136192,
+ "endTime": 1768892267580,
+ "criteriaId": "696660bde56b708ddc8dfc5f",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbf7": {
+ "qid": "696660bde56b708ddc8dfbf7",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Other income source",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136201,
+ "endTime": 1768892269177,
+ "criteriaId": "696660bde56b708ddc8dfc5f",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbf8": {
+ "qid": "696660bde56b708ddc8dfbf8",
+ "value": "hello",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Please specify other income source",
+ ""
+ ],
+ "labels": [
+ "hello"
+ ],
+ "responseType": "text",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136214,
+ "endTime": 1768892271723,
+ "criteriaId": "696660bde56b708ddc8dfc5f",
+ "responseType": "text",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbf9": {
+ "qid": "696660bde56b708ddc8dfbf9",
+ "value": [
+ {
+ "696660bde56b708ddc8dfbfa": {
+ "qid": "696660bde56b708ddc8dfbfa",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Asset Type",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": "",
+ "endTime": 1768892281734,
+ "criteriaId": "696660bde56b708ddc8dfc61",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbfb": {
+ "qid": "696660bde56b708ddc8dfbfb",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Specify Other Asset",
+ ""
+ ],
+ "responseType": "text",
+ "filesNotUploaded": []
+ },
+ "startTime": "",
+ "endTime": "",
+ "criteriaId": "696660bde56b708ddc8dfc61",
+ "responseType": "text",
+ "evidenceMethod": "OB",
+ "visibleIf": [
+ {
+ "operator": "===",
+ "value": "R13",
+ "_id": "696660bde56b708ddc8dfbfa"
+ }
+ ],
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbfc": {
+ "qid": "696660bde56b708ddc8dfbfc",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Quantity owned by household",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": "",
+ "endTime": 1768892283774,
+ "criteriaId": "696660bde56b708ddc8dfc61",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbfd": {
+ "qid": "696660bde56b708ddc8dfbfd",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Quantity owned by you",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": "",
+ "endTime": 1768892285684,
+ "criteriaId": "696660bde56b708ddc8dfc61",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbfe": {
+ "qid": "696660bde56b708ddc8dfbfe",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Value per unit (R)",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": "",
+ "endTime": 1768892288414,
+ "criteriaId": "696660bde56b708ddc8dfc61",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ }
+ }
+ ],
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Asset Inventory",
+ ""
+ ],
+ "labels": [
+ [
+ {
+ "_id": "696660bde56b708ddc8dfbfa",
+ "externalId": "Q75_4766403683020-1768317117023",
+ "question": [
+ "Asset Type",
+ ""
+ ],
+ "tip": "",
+ "hint": "",
+ "responseType": "select-dropdown",
+ "value": "R1",
+ "isCompleted": false,
+ "showRemarks": false,
+ "remarks": "",
+ "visibleIf": "",
+ "options": [
+ {
+ "value": "R1",
+ "label": "Productive tools and equipment"
+ },
+ {
+ "value": "R2",
+ "label": "Land"
+ },
+ {
+ "value": "R3",
+ "label": "Cattle"
+ },
+ {
+ "value": "R4",
+ "label": "Horses"
+ },
+ {
+ "value": "R5",
+ "label": "Goats"
+ },
+ {
+ "value": "R6",
+ "label": "Sheep"
+ },
+ {
+ "value": "R7",
+ "label": "Chicken/duck/poultry"
+ },
+ {
+ "value": "R8",
+ "label": "Cell phone (smartphone)"
+ },
+ {
+ "value": "R9",
+ "label": "Cell phone (old)"
+ },
+ {
+ "value": "R10",
+ "label": "Radio"
+ },
+ {
+ "value": "R11",
+ "label": "Television"
+ },
+ {
+ "value": "R12",
+ "label": "Computer"
+ },
+ {
+ "value": "R13",
+ "label": "Other"
+ }
+ ],
+ "sliderOptions": [],
+ "children": [
+ "696660bde56b708ddc8dfbfb"
+ ],
+ "questionGroup": [
+ "A1"
+ ],
+ "questionType": "auto",
+ "modeOfCollection": "",
+ "usedForScoring": "",
+ "fileName": [],
+ "validation": {
+ "required": false
+ },
+ "accessibility": "",
+ "canBeNotApplicable": "false",
+ "instanceQuestions": [],
+ "isAGeneralQuestion": false,
+ "autoCapture": false,
+ "rubricLevel": "",
+ "sectionHeader": "",
+ "allowAudioRecording": false,
+ "page": "p4",
+ "questionNumber": "",
+ "prefillFromEntityProfile": false,
+ "entityFieldName": "",
+ "isEditable": true,
+ "showQuestionInPreview": false,
+ "createdFromQuestionId": "69666080e56b708ddc8dfb89",
+ "orgId": "brac_gbl",
+ "tenantId": "brac",
+ "reportType": "default",
+ "updatedAt": "2026-01-13T15:11:57.036Z",
+ "createdAt": "2026-01-13T15:10:56.812Z",
+ "deleted": false,
+ "__v": 0,
+ "evidenceMethod": "OB",
+ "payload": {
+ "criteriaId": "696660bde56b708ddc8dfc61",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "rubricLevel": ""
+ },
+ "startTime": "",
+ "endTime": 1768892281734,
+ "gpsLocation": "",
+ "file": ""
+ },
+ {
+ "_id": "696660bde56b708ddc8dfbfb",
+ "externalId": "Q76_4766403683020-1768317117024",
+ "question": [
+ "Specify Other Asset",
+ ""
+ ],
+ "tip": "",
+ "hint": "",
+ "responseType": "text",
+ "isCompleted": false,
+ "showRemarks": false,
+ "remarks": "",
+ "visibleIf": [
+ {
+ "operator": "===",
+ "value": "R13",
+ "_id": "696660bde56b708ddc8dfbfa"
+ }
+ ],
+ "options": [],
+ "sliderOptions": [],
+ "children": [],
+ "questionGroup": [
+ "A1"
+ ],
+ "questionType": "auto",
+ "modeOfCollection": "",
+ "usedForScoring": "",
+ "fileName": [],
+ "validation": {
+ "required": false
+ },
+ "accessibility": "",
+ "canBeNotApplicable": "false",
+ "instanceQuestions": [],
+ "isAGeneralQuestion": false,
+ "autoCapture": false,
+ "rubricLevel": "",
+ "sectionHeader": "",
+ "allowAudioRecording": false,
+ "page": "p4",
+ "questionNumber": "",
+ "prefillFromEntityProfile": false,
+ "entityFieldName": "",
+ "isEditable": true,
+ "showQuestionInPreview": false,
+ "createdFromQuestionId": "69666080e56b708ddc8dfb90",
+ "orgId": "brac_gbl",
+ "tenantId": "brac",
+ "reportType": "default",
+ "updatedAt": "2026-01-13T15:11:57.036Z",
+ "createdAt": "2026-01-13T15:10:56.831Z",
+ "deleted": false,
+ "__v": 0,
+ "evidenceMethod": "OB",
+ "payload": {
+ "criteriaId": "696660bde56b708ddc8dfc61",
+ "responseType": "text",
+ "evidenceMethod": "OB",
+ "rubricLevel": ""
+ },
+ "startTime": "",
+ "endTime": "",
+ "gpsLocation": "",
+ "file": ""
+ },
+ {
+ "_id": "696660bde56b708ddc8dfbfc",
+ "externalId": "Q77_4766403683020-1768317117025",
+ "question": [
+ "Quantity owned by household",
+ ""
+ ],
+ "tip": "",
+ "hint": "",
+ "responseType": "number",
+ "value": 1,
+ "isCompleted": false,
+ "showRemarks": false,
+ "remarks": "",
+ "visibleIf": "",
+ "options": [],
+ "sliderOptions": [],
+ "children": [],
+ "questionGroup": [
+ "A1"
+ ],
+ "questionType": "auto",
+ "modeOfCollection": "",
+ "usedForScoring": "",
+ "fileName": [],
+ "validation": {
+ "required": false,
+ "IsNumber": "true"
+ },
+ "accessibility": "",
+ "canBeNotApplicable": "false",
+ "instanceQuestions": [],
+ "isAGeneralQuestion": false,
+ "autoCapture": false,
+ "rubricLevel": "",
+ "sectionHeader": "",
+ "allowAudioRecording": false,
+ "page": "p4",
+ "questionNumber": "",
+ "prefillFromEntityProfile": false,
+ "entityFieldName": "",
+ "isEditable": true,
+ "showQuestionInPreview": false,
+ "createdFromQuestionId": "69666080e56b708ddc8dfb98",
+ "orgId": "brac_gbl",
+ "tenantId": "brac",
+ "reportType": "default",
+ "updatedAt": "2026-01-13T15:11:57.036Z",
+ "createdAt": "2026-01-13T15:10:56.856Z",
+ "deleted": false,
+ "__v": 0,
+ "evidenceMethod": "OB",
+ "payload": {
+ "criteriaId": "696660bde56b708ddc8dfc61",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "rubricLevel": ""
+ },
+ "startTime": "",
+ "endTime": 1768892283774,
+ "gpsLocation": "",
+ "file": ""
+ },
+ {
+ "_id": "696660bde56b708ddc8dfbfd",
+ "externalId": "Q78_4766403683020-1768317117026",
+ "question": [
+ "Quantity owned by you",
+ ""
+ ],
+ "tip": "",
+ "hint": "",
+ "responseType": "number",
+ "value": 1,
+ "isCompleted": false,
+ "showRemarks": false,
+ "remarks": "",
+ "visibleIf": "",
+ "options": [],
+ "sliderOptions": [],
+ "children": [],
+ "questionGroup": [
+ "A1"
+ ],
+ "questionType": "auto",
+ "modeOfCollection": "",
+ "usedForScoring": "",
+ "fileName": [],
+ "validation": {
+ "required": false,
+ "IsNumber": "true"
+ },
+ "accessibility": "",
+ "canBeNotApplicable": "false",
+ "instanceQuestions": [],
+ "isAGeneralQuestion": false,
+ "autoCapture": false,
+ "rubricLevel": "",
+ "sectionHeader": "",
+ "allowAudioRecording": false,
+ "page": "p4",
+ "questionNumber": "",
+ "prefillFromEntityProfile": false,
+ "entityFieldName": "",
+ "isEditable": true,
+ "showQuestionInPreview": false,
+ "createdFromQuestionId": "69666080e56b708ddc8dfb9f",
+ "orgId": "brac_gbl",
+ "tenantId": "brac",
+ "reportType": "default",
+ "updatedAt": "2026-01-13T15:11:57.036Z",
+ "createdAt": "2026-01-13T15:10:56.877Z",
+ "deleted": false,
+ "__v": 0,
+ "evidenceMethod": "OB",
+ "payload": {
+ "criteriaId": "696660bde56b708ddc8dfc61",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "rubricLevel": ""
+ },
+ "startTime": "",
+ "endTime": 1768892285684,
+ "gpsLocation": "",
+ "file": ""
+ },
+ {
+ "_id": "696660bde56b708ddc8dfbfe",
+ "externalId": "Q79_4766403683020-1768317117026",
+ "question": [
+ "Value per unit (R)",
+ ""
+ ],
+ "tip": "",
+ "hint": "",
+ "responseType": "number",
+ "value": 1,
+ "isCompleted": false,
+ "showRemarks": false,
+ "remarks": "",
+ "visibleIf": "",
+ "options": [],
+ "sliderOptions": [],
+ "children": [],
+ "questionGroup": [
+ "A1"
+ ],
+ "questionType": "auto",
+ "modeOfCollection": "",
+ "usedForScoring": "",
+ "fileName": [],
+ "validation": {
+ "required": false,
+ "IsNumber": "true"
+ },
+ "accessibility": "",
+ "canBeNotApplicable": "false",
+ "instanceQuestions": [],
+ "isAGeneralQuestion": false,
+ "autoCapture": false,
+ "rubricLevel": "",
+ "sectionHeader": "",
+ "allowAudioRecording": false,
+ "page": "p4",
+ "questionNumber": "",
+ "prefillFromEntityProfile": false,
+ "entityFieldName": "",
+ "isEditable": true,
+ "showQuestionInPreview": false,
+ "createdFromQuestionId": "69666080e56b708ddc8dfba6",
+ "orgId": "brac_gbl",
+ "tenantId": "brac",
+ "reportType": "default",
+ "updatedAt": "2026-01-13T15:11:57.036Z",
+ "createdAt": "2026-01-13T15:10:56.909Z",
+ "deleted": false,
+ "__v": 0,
+ "evidenceMethod": "OB",
+ "payload": {
+ "criteriaId": "696660bde56b708ddc8dfc61",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "rubricLevel": ""
+ },
+ "startTime": "",
+ "endTime": 1768892288414,
+ "gpsLocation": "",
+ "file": ""
+ }
+ ]
+ ],
+ "responseType": "matrix",
+ "filesNotUploaded": []
+ },
+ "startTime": "",
+ "endTime": "",
+ "criteriaId": "696660bde56b708ddc8dfc61",
+ "responseType": "matrix",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "countOfInstances": 1
+ }
+ },
+ "startTime": 1768894219120,
+ "endTime": 1768894222118,
+ "isSubmitted": false,
+ "gpsLocation": null,
+ "submittedBy": "3087",
+ "submittedByName": "Farabi Ahmedullah",
+ "submittedByEmail": null,
+ "submissionDate": "2026-01-20T07:30:24.523Z",
+ "isValid": true
+ }
+ ],
+ "hasConflicts": false
+ }
+ },
+ "criteria": [
+ {
+ "_id": "696660bde56b708ddc8dfc55",
+ "orgId": "brac_gbl",
+ "tenantId": "brac",
+ "__v": 0,
+ "createdAt": "2026-01-13T15:11:57.240Z",
+ "criteriaType": "manual",
+ "deleted": false,
+ "description": "Facilitator & Administrative Details",
+ "externalId": "TDCL1_3765453411688-1768317117215",
+ "flag": "",
+ "frameworkCriteriaId": "69665ea0e56b708ddc8df9a3",
+ "name": "Facilitator & Administrative Details",
+ "owner": "3092",
+ "remarks": "",
+ "rubric": {
+ "name": "Facilitator & Administrative Details",
+ "description": "Facilitator & Administrative Details",
+ "type": "auto",
+ "levels": {
+ "L1": {
+ "level": "L1",
+ "label": "Level 1",
+ "description": "NA",
+ "expression": ""
+ }
+ }
+ },
+ "score": "",
+ "showRemarks": null,
+ "timesUsed": 12,
+ "updatedAt": "2026-01-13T15:11:57.240Z",
+ "weightage": 100
+ },
+ {
+ "_id": "696660bde56b708ddc8dfc57",
+ "orgId": "brac_gbl",
+ "tenantId": "brac",
+ "__v": 0,
+ "createdAt": "2026-01-13T15:11:57.257Z",
+ "criteriaType": "manual",
+ "deleted": false,
+ "description": "Participant Details",
+ "externalId": "TDCL1_4765453411688-1768317117222",
+ "flag": "",
+ "frameworkCriteriaId": "69665ea0e56b708ddc8df9a4",
+ "name": "Participant Details",
+ "owner": "3092",
+ "remarks": "",
+ "rubric": {
+ "name": "Participant Details",
+ "description": "Participant Details",
+ "type": "auto",
+ "levels": {
+ "L1": {
+ "level": "L1",
+ "label": "Level 1",
+ "description": "NA",
+ "expression": ""
+ }
+ }
+ },
+ "score": "",
+ "showRemarks": null,
+ "timesUsed": 12,
+ "updatedAt": "2026-01-13T15:11:57.257Z",
+ "weightage": 100
+ },
+ {
+ "_id": "696660bde56b708ddc8dfc59",
+ "orgId": "brac_gbl",
+ "tenantId": "brac",
+ "__v": 0,
+ "createdAt": "2026-01-13T15:11:57.274Z",
+ "criteriaType": "manual",
+ "deleted": false,
+ "description": "Household Composition",
+ "externalId": "TDCL1_5765453411688-1768317117225",
+ "flag": "",
+ "frameworkCriteriaId": "69665ea0e56b708ddc8df9a5",
+ "name": "Household Composition",
+ "owner": "3092",
+ "remarks": "",
+ "rubric": {
+ "name": "Household Composition",
+ "description": "Household Composition",
+ "type": "auto",
+ "levels": {
+ "L1": {
+ "level": "L1",
+ "label": "Level 1",
+ "description": "NA",
+ "expression": ""
+ }
+ }
+ },
+ "score": "",
+ "showRemarks": null,
+ "timesUsed": 12,
+ "updatedAt": "2026-01-13T15:11:57.274Z",
+ "weightage": 100
+ },
+ {
+ "_id": "696660bde56b708ddc8dfc5b",
+ "orgId": "brac_gbl",
+ "tenantId": "brac",
+ "__v": 0,
+ "createdAt": "2026-01-13T15:11:57.295Z",
+ "criteriaType": "manual",
+ "deleted": false,
+ "description": "Government Grants",
+ "externalId": "TDCL1_6765453411688-1768317117227",
+ "flag": "",
+ "frameworkCriteriaId": "69665ea0e56b708ddc8df9a6",
+ "name": "Government Grants",
+ "owner": "3092",
+ "remarks": "",
+ "rubric": {
+ "name": "Government Grants",
+ "description": "Government Grants",
+ "type": "auto",
+ "levels": {
+ "L1": {
+ "level": "L1",
+ "label": "Level 1",
+ "description": "NA",
+ "expression": ""
+ }
+ }
+ },
+ "score": "",
+ "showRemarks": null,
+ "timesUsed": 12,
+ "updatedAt": "2026-01-13T15:11:57.295Z",
+ "weightage": 100
+ },
+ {
+ "_id": "696660bde56b708ddc8dfc5d",
+ "orgId": "brac_gbl",
+ "tenantId": "brac",
+ "__v": 0,
+ "createdAt": "2026-01-13T15:11:57.311Z",
+ "criteriaType": "manual",
+ "deleted": false,
+ "description": "Household Income Sources",
+ "externalId": "TDCL1_7765453411688-1768317117230",
+ "flag": "",
+ "frameworkCriteriaId": "69665ea0e56b708ddc8df9a7",
+ "name": "Household Income Sources",
+ "owner": "3092",
+ "remarks": "",
+ "rubric": {
+ "name": "Household Income Sources",
+ "description": "Household Income Sources",
+ "type": "auto",
+ "levels": {
+ "L1": {
+ "level": "L1",
+ "label": "Level 1",
+ "description": "NA",
+ "expression": ""
+ }
+ }
+ },
+ "score": "",
+ "showRemarks": null,
+ "timesUsed": 12,
+ "updatedAt": "2026-01-13T15:11:57.311Z",
+ "weightage": 100
+ },
+ {
+ "_id": "696660bde56b708ddc8dfc5f",
+ "orgId": "brac_gbl",
+ "tenantId": "brac",
+ "__v": 0,
+ "createdAt": "2026-01-13T15:11:57.325Z",
+ "criteriaType": "manual",
+ "deleted": false,
+ "description": "Participant Income Sources",
+ "externalId": "TDCL1_8765453411688-1768317117232",
+ "flag": "",
+ "frameworkCriteriaId": "69665ea0e56b708ddc8df9a8",
+ "name": "Participant Income Sources",
+ "owner": "3092",
+ "remarks": "",
+ "rubric": {
+ "name": "Participant Income Sources",
+ "description": "Participant Income Sources",
+ "type": "auto",
+ "levels": {
+ "L1": {
+ "level": "L1",
+ "label": "Level 1",
+ "description": "NA",
+ "expression": ""
+ }
+ }
+ },
+ "score": "",
+ "showRemarks": null,
+ "timesUsed": 12,
+ "updatedAt": "2026-01-13T15:11:57.325Z",
+ "weightage": 100
+ },
+ {
+ "_id": "696660bde56b708ddc8dfc61",
+ "orgId": "brac_gbl",
+ "tenantId": "brac",
+ "__v": 0,
+ "createdAt": "2026-01-13T15:11:57.339Z",
+ "criteriaType": "manual",
+ "deleted": false,
+ "description": "Assets",
+ "externalId": "TDCL1_9765453411688-1768317117234",
+ "flag": "",
+ "frameworkCriteriaId": "69665ea0e56b708ddc8df9a9",
+ "name": "Assets",
+ "owner": "3092",
+ "remarks": "",
+ "rubric": {
+ "name": "Assets",
+ "description": "Assets",
+ "type": "auto",
+ "levels": {
+ "L1": {
+ "level": "L1",
+ "label": "Level 1",
+ "description": "NA",
+ "expression": ""
+ }
+ }
+ },
+ "score": "",
+ "showRemarks": null,
+ "timesUsed": 12,
+ "updatedAt": "2026-01-13T15:11:57.339Z",
+ "weightage": 100
+ }
+ ],
+ "themes": [
+ {
+ "type": "theme",
+ "label": "theme",
+ "name": "Observation Theme",
+ "externalId": "OB",
+ "weightage": 100,
+ "criteria": [
+ {
+ "criteriaId": "696660bde56b708ddc8dfc55",
+ "weightage": 100
+ },
+ {
+ "criteriaId": "696660bde56b708ddc8dfc57",
+ "weightage": 100
+ },
+ {
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "weightage": 100
+ },
+ {
+ "criteriaId": "696660bde56b708ddc8dfc5b",
+ "weightage": 100
+ },
+ {
+ "criteriaId": "696660bde56b708ddc8dfc5d",
+ "weightage": 100
+ },
+ {
+ "criteriaId": "696660bde56b708ddc8dfc5f",
+ "weightage": 100
+ },
+ {
+ "criteriaId": "696660bde56b708ddc8dfc61",
+ "weightage": 100
+ }
+ ]
+ }
+ ],
+ "entityExternalId": "3090",
+ "entityInformation": {
+ "targetedEntityTypes": [],
+ "externalId": "3090",
+ "name": "Suvarna Kale",
+ "onBoardingProjectId": "696492a35f21a60014b6c4c5",
+ "status": "ONBOARDED",
+ "registryDetails": {
+ "code": "3090",
+ "locationId": "3090"
+ },
+ "parentInformation": {
+ "linkageChampion": [
+ {
+ "_id": "6953d07fe83c1c0014713c00",
+ "externalId": "3087",
+ "name": "Farabi Ahmedullah"
+ }
+ ]
+ },
+ "_id": "6953d07fe83c1c0014713c05",
+ "type": "participant",
+ "typeId": "6953d07ee83c1c0014713bf8"
+ },
+ "observationInformation": {
+ "name": "Household Profile",
+ "description": "Household Profile",
+ "createdBy": "3087",
+ "frameworkId": "69665f69e56b708ddc8df9b2",
+ "frameworkExternalId": "9ec11dfa-d787-11f0-ac92-4cd717431bdc",
+ "solutionId": "696660bde56b708ddc8dfc71",
+ "solutionExternalId": "9ec11dfa-d787-11f0-ac92-4cd717431bdc-OBSERVATION-TEMPLATE_CHILD",
+ "startDate": "2026-01-20T04:28:25.047Z",
+ "endDate": "2027-01-20T04:28:25.047Z",
+ "status": "published",
+ "entityTypeId": "6953d07ee83c1c0014713bf8",
+ "entityType": "participant",
+ "createdFor": [
+ "3087"
+ ],
+ "rootOrganisations": [],
+ "isAPrivateProgram": false,
+ "link": "d0fd0f793173c1dac39775504b2ecb22",
+ "isExternalProgram": false,
+ "userProfile": {
+ "id": "3087",
+ "email_verified": "false",
+ "name": "Farabi Ahmedullah",
+ "username": "farabi",
+ "phone_code": "91",
+ "about": "my self farabi",
+ "dob": "22-12-1990",
+ "share_link": null,
+ "status": "ACTIVE",
+ "image": null,
+ "has_accepted_terms_and_conditions": false,
+ "languages": null,
+ "preferred_language": "en",
+ "tenant_code": "brac",
+ "meta": null,
+ "created_at": "2025-12-29T06:28:30.499Z",
+ "updated_at": "2026-01-09T11:59:33.769Z",
+ "deleted_at": null,
+ "organizations": [
+ {
+ "id": "67",
+ "name": "BRAC GBL org",
+ "code": "brac_gbl",
+ "description": "BRAC GBL org",
+ "status": "ACTIVE",
+ "related_orgs": null,
+ "tenant_code": "brac",
+ "meta": null,
+ "created_by": null,
+ "updated_by": 1,
+ "roles": [
+ {
+ "id": "213",
+ "title": "session_manager",
+ "label": "Linkage Champion",
+ "user_type": 0,
+ "status": "ACTIVE",
+ "organization_id": "67",
+ "visibility": "PUBLIC",
+ "tenant_code": "brac"
+ },
+ {
+ "id": "214",
+ "title": "org_admin",
+ "label": "Supervisor",
+ "user_type": 0,
+ "status": "ACTIVE",
+ "organization_id": "67",
+ "visibility": "PUBLIC",
+ "tenant_code": "brac"
+ }
+ ]
+ }
+ ],
+ "permissions": [
+ {
+ "module": "user",
+ "request_type": [
+ "POST",
+ "DELETE",
+ "GET",
+ "PUT",
+ "PATCH"
+ ],
+ "service": "user"
+ },
+ {
+ "module": "form",
+ "request_type": [
+ "POST",
+ "DELETE",
+ "GET",
+ "PUT",
+ "PATCH"
+ ],
+ "service": "user"
+ },
+ {
+ "module": "cloud-services",
+ "request_type": [
+ "POST",
+ "DELETE",
+ "GET",
+ "PUT",
+ "PATCH"
+ ],
+ "service": "user"
+ },
+ {
+ "module": "organization",
+ "request_type": [
+ "POST",
+ "GET"
+ ],
+ "service": "user"
+ },
+ {
+ "module": "entity-type",
+ "request_type": [
+ "POST",
+ "DELETE",
+ "GET",
+ "PUT",
+ "PATCH"
+ ],
+ "service": "user"
+ },
+ {
+ "module": "entity",
+ "request_type": [
+ "POST",
+ "DELETE",
+ "PUT",
+ "PATCH",
+ "GET"
+ ],
+ "service": "user"
+ },
+ {
+ "module": "org-admin",
+ "request_type": [
+ "POST",
+ "DELETE",
+ "GET",
+ "PUT",
+ "PATCH"
+ ],
+ "service": "user"
+ },
+ {
+ "module": "notification",
+ "request_type": [
+ "POST",
+ "DELETE",
+ "GET",
+ "PUT",
+ "PATCH"
+ ],
+ "service": "user"
+ },
+ {
+ "module": "account",
+ "request_type": [
+ "GET",
+ "POST"
+ ],
+ "service": "user"
+ },
+ {
+ "module": "user-role",
+ "request_type": [
+ "GET",
+ "POST",
+ "DELETE",
+ "PUT",
+ "PATCH"
+ ],
+ "service": "user"
+ },
+ {
+ "module": "organization-feature",
+ "request_type": [
+ "POST",
+ "GET",
+ "PATCH",
+ "DELETE"
+ ],
+ "service": "user"
+ },
+ {
+ "module": "feature",
+ "request_type": [
+ "GET"
+ ],
+ "service": "user"
+ }
+ ],
+ "image_cloud_path": null
+ },
+ "orgId": "brac_gbl",
+ "tenantId": "brac",
+ "updatedAt": "2026-01-20T04:28:25.230Z",
+ "createdAt": "2026-01-13T15:11:57.344Z"
+ },
+ "feedback": [],
+ "solutionId": "696660bde56b708ddc8dfc71",
+ "solutionExternalId": "9ec11dfa-d787-11f0-ac92-4cd717431bdc-OBSERVATION-TEMPLATE_CHILD",
+ "submissionsUpdatedHistory": [],
+ "entityType": "participant",
+ "programId": null,
+ "programExternalId": null,
+ "submissionNumber": 1,
+ "pointsBasedMaxScore": 0,
+ "pointsBasedScoreAchieved": 0,
+ "pointsBasedPercentageScore": 0,
+ "isAPrivateProgram": false,
+ "scoringSystem": null,
+ "isRubricDriven": false,
+ "userProfile": {
+ "id": "3087",
+ "email_verified": "false",
+ "name": "Farabi Ahmedullah Two",
+ "username": "farabi",
+ "phone_code": "91",
+ "about": "my self farabi Two",
+ "dob": "22-12-2000",
+ "share_link": null,
+ "status": "ACTIVE",
+ "image": null,
+ "has_accepted_terms_and_conditions": false,
+ "languages": null,
+ "preferred_language": "en",
+ "tenant_code": "brac",
+ "meta": null,
+ "created_at": "2025-12-29T06:28:30.499Z",
+ "updated_at": "2026-01-09T11:59:33.769Z",
+ "deleted_at": null,
+ "organizations": [
+ {
+ "id": "67",
+ "name": "BRAC GBL org",
+ "code": "brac_gbl",
+ "description": "BRAC GBL org",
+ "status": "ACTIVE",
+ "related_orgs": null,
+ "tenant_code": "brac",
+ "meta": null,
+ "created_by": null,
+ "updated_by": 1,
+ "roles": [
+ {
+ "id": "213",
+ "title": "session_manager",
+ "label": "Linkage Champion",
+ "user_type": 0,
+ "status": "ACTIVE",
+ "organization_id": "67",
+ "visibility": "PUBLIC",
+ "tenant_code": "brac"
+ },
+ {
+ "id": "214",
+ "title": "org_admin",
+ "label": "Supervisor",
+ "user_type": 0,
+ "status": "ACTIVE",
+ "organization_id": "67",
+ "visibility": "PUBLIC",
+ "tenant_code": "brac"
+ }
+ ]
+ }
+ ],
+ "permissions": [
+ {
+ "module": "user",
+ "request_type": [
+ "POST",
+ "DELETE",
+ "GET",
+ "PUT",
+ "PATCH"
+ ],
+ "service": "user"
+ },
+ {
+ "module": "form",
+ "request_type": [
+ "POST",
+ "DELETE",
+ "GET",
+ "PUT",
+ "PATCH"
+ ],
+ "service": "user"
+ },
+ {
+ "module": "cloud-services",
+ "request_type": [
+ "POST",
+ "DELETE",
+ "GET",
+ "PUT",
+ "PATCH"
+ ],
+ "service": "user"
+ },
+ {
+ "module": "organization",
+ "request_type": [
+ "POST",
+ "GET"
+ ],
+ "service": "user"
+ },
+ {
+ "module": "entity-type",
+ "request_type": [
+ "POST",
+ "DELETE",
+ "GET",
+ "PUT",
+ "PATCH"
+ ],
+ "service": "user"
+ },
+ {
+ "module": "entity",
+ "request_type": [
+ "POST",
+ "DELETE",
+ "PUT",
+ "PATCH",
+ "GET"
+ ],
+ "service": "user"
+ },
+ {
+ "module": "org-admin",
+ "request_type": [
+ "POST",
+ "DELETE",
+ "GET",
+ "PUT",
+ "PATCH"
+ ],
+ "service": "user"
+ },
+ {
+ "module": "notification",
+ "request_type": [
+ "POST",
+ "DELETE",
+ "GET",
+ "PUT",
+ "PATCH"
+ ],
+ "service": "user"
+ },
+ {
+ "module": "account",
+ "request_type": [
+ "GET",
+ "POST"
+ ],
+ "service": "user"
+ },
+ {
+ "module": "user-role",
+ "request_type": [
+ "GET",
+ "POST",
+ "DELETE",
+ "PUT",
+ "PATCH"
+ ],
+ "service": "user"
+ },
+ {
+ "module": "organization-feature",
+ "request_type": [
+ "POST",
+ "GET",
+ "PATCH",
+ "DELETE"
+ ],
+ "service": "user"
+ },
+ {
+ "module": "feature",
+ "request_type": [
+ "GET"
+ ],
+ "service": "user"
+ }
+ ],
+ "image_cloud_path": null
+ },
+ "programInformation": null,
+ "orgId": "brac_gbl",
+ "tenantId": "brac",
+ "isExternalProgram": false,
+ "deleted": false,
+ "title": "Observation 1",
+ "updatedAt": "2026-01-20T07:30:24.603Z",
+ "createdAt": "2026-01-20T04:28:25.528Z",
+ "__v": 0,
+ "answers": {
+ "696660cdf56b708ddc8dfbb2": {
+ "qid": "696660cdf56b708ddc8dfbb2",
+ "value": "sagar",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Facilitator Name",
+ ""
+ ],
+ "labels": [
+ "sagar"
+ ],
+ "responseType": "text",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135767,
+ "endTime": 1768892119361,
+ "criteriaId": "696660bde56b708ddc8dfc55",
+ "responseType": "text",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q1_4766403683020-1768917116969",
+ "question": [
+ "Facilitator Name",
+ ""
+ ],
+ "questionNumber": "",
+ "reportType": "default"
+ },
+ "696660bce56b708ddc8dfbb2": {
+ "qid": "696660bce56b708ddc8dfbb2",
+ "value": "good",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Province",
+ ""
+ ],
+ "labels": [
+ "good"
+ ],
+ "responseType": "text",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135789,
+ "endTime": 1768892124079,
+ "criteriaId": "696660bde56b708ddc8dfc55",
+ "responseType": "text",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q2_4766403683020-1768317116969",
+ "question": [
+ "Province",
+ ""
+ ],
+ "questionNumber": "2",
+ "reportType": "default"
+ },
+ "696660bce56b708ddc8dfbb3": {
+ "qid": "696660bce56b708ddc8dfbb3",
+ "value": "20",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Pilot Site",
+ ""
+ ],
+ "labels": [
+ "20"
+ ],
+ "responseType": "text",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135796,
+ "endTime": 1768892127781,
+ "criteriaId": "696660bde56b708ddc8dfc55",
+ "responseType": "text",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q3_4766403683020-1768317116970",
+ "question": [
+ "Pilot Site",
+ ""
+ ],
+ "questionNumber": "3",
+ "reportType": "default"
+ },
+ "696660bce56b708ddc8dfbb4": {
+ "qid": "696660bce56b708ddc8dfbb4",
+ "value": "2026-01-20T06:55:00.000Z",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Date of Collection",
+ ""
+ ],
+ "labels": [
+ "2026-01-20T06:55:00.000Z"
+ ],
+ "responseType": "date",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135803,
+ "endTime": 1768892129231,
+ "criteriaId": "696660bde56b708ddc8dfc55",
+ "responseType": "date",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q4_4766403683020-1768317116971",
+ "question": [
+ "Date of Collection",
+ ""
+ ],
+ "questionNumber": "4",
+ "reportType": "default"
+ },
+ "696660bce56b708ddc8dfbb5": {
+ "qid": "696660bce56b708ddc8dfbb5",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Record if the captured residential address is correct",
+ ""
+ ],
+ "labels": [
+ "Yes"
+ ],
+ "responseType": "radio",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135820,
+ "endTime": 1768892136961,
+ "criteriaId": "696660bde56b708ddc8dfc57",
+ "responseType": "radio",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [
+ {
+ "value": "R1",
+ "label": "Yes"
+ },
+ {
+ "value": "R2",
+ "label": "No"
+ }
+ ],
+ "externalId": "Q5_4766403683020-1768317116971",
+ "question": [
+ "Record if the captured residential address is correct",
+ ""
+ ],
+ "questionNumber": "1",
+ "reportType": "default"
+ },
+ "696660bce56b708ddc8dfbb6": {
+ "qid": "696660bce56b708ddc8dfbb6",
+ "value": "vaibhav",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "What is your name?",
+ ""
+ ],
+ "labels": [
+ "vaibhav"
+ ],
+ "responseType": "text",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135827,
+ "endTime": 1768892300964,
+ "criteriaId": "696660bde56b708ddc8dfc57",
+ "responseType": "text",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q6_4766403683020-1768317116972",
+ "question": [
+ "What is your name?",
+ ""
+ ],
+ "questionNumber": "2",
+ "reportType": "default"
+ },
+ "696660bce56b708ddc8dfbb7": {
+ "qid": "696660bce56b708ddc8dfbb7",
+ "value": "123",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "What is your ID number?",
+ ""
+ ],
+ "labels": [
+ "123"
+ ],
+ "responseType": "text",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135845,
+ "endTime": 1768892306434,
+ "criteriaId": "696660bde56b708ddc8dfc57",
+ "responseType": "text",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q7_4766403683020-1768317116973",
+ "question": [
+ "What is your ID number?",
+ ""
+ ],
+ "questionNumber": "3",
+ "reportType": "default"
+ },
+ "696660bce56b708ddc8dfbb8": {
+ "qid": "696660bce56b708ddc8dfbb8",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Is the respondent a man or a woman? (record from observation)",
+ ""
+ ],
+ "labels": [
+ "Man (Male)"
+ ],
+ "responseType": "radio",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135850,
+ "endTime": 1768892306546,
+ "criteriaId": "696660bde56b708ddc8dfc57",
+ "responseType": "radio",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [
+ {
+ "value": "R1",
+ "label": "Man (Male)"
+ },
+ {
+ "value": "R2",
+ "label": "Woman (Female)"
+ },
+ {
+ "value": "R3",
+ "label": "Other"
+ }
+ ],
+ "externalId": "Q8_4766403683020-1768317116974",
+ "question": [
+ "Is the respondent a man or a woman? (record from observation)",
+ ""
+ ],
+ "questionNumber": "4",
+ "reportType": "default"
+ },
+ "696660bce56b708ddc8dfbb9": {
+ "qid": "696660bce56b708ddc8dfbb9",
+ "value": "9876543210",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "What is your cell phone number?",
+ ""
+ ],
+ "labels": [
+ "9876543210"
+ ],
+ "responseType": "text",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135855,
+ "endTime": 1768892315705,
+ "criteriaId": "696660bde56b708ddc8dfc57",
+ "responseType": "text",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q9_4766403683020-1768317116974",
+ "question": [
+ "What is your cell phone number?",
+ ""
+ ],
+ "questionNumber": "5",
+ "reportType": "default"
+ },
+ "696660bce56b708ddc8dfbba": {
+ "qid": "696660bce56b708ddc8dfbba",
+ "value": "9087654321",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Please may I have an alternative number for you?",
+ ""
+ ],
+ "labels": [
+ "9087654321"
+ ],
+ "responseType": "text",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135859,
+ "endTime": 1768892325383,
+ "criteriaId": "696660bde56b708ddc8dfc57",
+ "responseType": "text",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q10_4766403683020-1768317116975",
+ "question": [
+ "Please may I have an alternative number for you?",
+ ""
+ ],
+ "questionNumber": "6",
+ "reportType": "default"
+ },
+ "696660bce56b708ddc8dfbbb": {
+ "qid": "696660bce56b708ddc8dfbbb",
+ "value": "sagar@gmail.com",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "And what is your email address?",
+ ""
+ ],
+ "labels": [
+ "sagar@gmail.com"
+ ],
+ "responseType": "text",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135865,
+ "endTime": 1768892330800,
+ "criteriaId": "696660bde56b708ddc8dfc57",
+ "responseType": "text",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q11_4766403683020-1768317116976",
+ "question": [
+ "And what is your email address?",
+ ""
+ ],
+ "questionNumber": "7",
+ "reportType": "default"
+ },
+ "696660bce56b708ddc8dfbbc": {
+ "qid": "696660bce56b708ddc8dfbbc",
+ "value": "R2",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Does the respondent have any disabilities?",
+ ""
+ ],
+ "labels": [
+ "No"
+ ],
+ "responseType": "radio",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135868,
+ "endTime": 1768892332857,
+ "criteriaId": "696660bde56b708ddc8dfc57",
+ "responseType": "radio",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [
+ {
+ "value": "R1",
+ "label": "Yes"
+ },
+ {
+ "value": "R2",
+ "label": "No"
+ }
+ ],
+ "externalId": "Q12_4766403683020-1768317116977",
+ "question": [
+ "Does the respondent have any disabilities?",
+ ""
+ ],
+ "questionNumber": "8",
+ "reportType": "default"
+ },
+ "696660bce56b708ddc8dfbbd": {
+ "qid": "696660bce56b708ddc8dfbbd",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Which disability?",
+ ""
+ ],
+ "responseType": "text",
+ "filesNotUploaded": []
+ },
+ "startTime": "",
+ "endTime": "",
+ "criteriaId": "696660bde56b708ddc8dfc57",
+ "responseType": "text",
+ "evidenceMethod": "OB",
+ "visibleIf": [
+ {
+ "operator": "===",
+ "value": "R1",
+ "_id": "696660bce56b708ddc8dfbbc"
+ }
+ ],
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q13_4766403683020-1768317116977",
+ "question": [
+ "Which disability?",
+ ""
+ ],
+ "questionNumber": "9",
+ "reportType": "default"
+ },
+ "696660bce56b708ddc8dfbbe": {
+ "qid": "696660bce56b708ddc8dfbbe",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Who is the head of this household?",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135873,
+ "endTime": 1768892334962,
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [
+ {
+ "value": "R1",
+ "label": "Participant"
+ },
+ {
+ "value": "R2",
+ "label": "Participant\\'s spouse (wife, husband) or partner"
+ },
+ {
+ "value": "R3",
+ "label": "Participant\\'s parent"
+ },
+ {
+ "value": "R4",
+ "label": "Participant\\'s brother or sister"
+ },
+ {
+ "value": "R5",
+ "label": "Another family member"
+ },
+ {
+ "value": "R6",
+ "label": "Participant and someone else share equally"
+ },
+ {
+ "value": "R7",
+ "label": "Other (specify)"
+ }
+ ],
+ "externalId": "Q14_4766403683020-1768317116978",
+ "question": [
+ "Who is the head of this household?",
+ ""
+ ],
+ "questionNumber": "1",
+ "reportType": "default"
+ },
+ "696660bce56b708ddc8dfbbf": {
+ "qid": "696660bce56b708ddc8dfbbf",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Please specify:",
+ ""
+ ],
+ "responseType": "text",
+ "filesNotUploaded": []
+ },
+ "startTime": "",
+ "endTime": "",
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "responseType": "text",
+ "evidenceMethod": "OB",
+ "visibleIf": [
+ {
+ "operator": "===",
+ "value": "R7",
+ "_id": "696660bce56b708ddc8dfbbe"
+ }
+ ],
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q15_4766403683020-1768317116979",
+ "question": [
+ "Please specify:",
+ ""
+ ],
+ "questionNumber": "2",
+ "reportType": "default"
+ },
+ "696660bce56b708ddc8dfbc0": {
+ "qid": "696660bce56b708ddc8dfbc0",
+ "value": "R2",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Do you have children under the age of 18 living with you here in this household?",
+ ""
+ ],
+ "labels": [
+ "No"
+ ],
+ "responseType": "radio",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135884,
+ "endTime": 1768892338963,
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "responseType": "radio",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [
+ {
+ "value": "R1",
+ "label": "Yes"
+ },
+ {
+ "value": "R2",
+ "label": "No"
+ }
+ ],
+ "externalId": "Q16_4766403683020-1768317116981",
+ "question": [
+ "Do you have children under the age of 18 living with you here in this household?",
+ ""
+ ],
+ "questionNumber": "3",
+ "reportType": "default"
+ },
+ "696660bce56b708ddc8dfbc1": {
+ "qid": "696660bce56b708ddc8dfbc1",
+ "value": [],
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Children Information",
+ ""
+ ],
+ "labels": [],
+ "responseType": "matrix",
+ "filesNotUploaded": []
+ },
+ "startTime": "",
+ "endTime": "",
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "responseType": "matrix",
+ "evidenceMethod": "OB",
+ "visibleIf": [
+ {
+ "operator": "===",
+ "value": "R1",
+ "_id": "696660bce56b708ddc8dfbc0"
+ }
+ ],
+ "rubricLevel": "",
+ "countOfInstances": 0,
+ "options": [],
+ "externalId": "Q17_4766403683020-1768317116981",
+ "question": [
+ "Children Information",
+ ""
+ ],
+ "questionNumber": "4",
+ "reportType": "default"
+ },
+ "696660bce56b708ddc8dfbc6": {
+ "qid": "696660bce56b708ddc8dfbc6",
+ "value": 19,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "How many other people over the age of 18 are living in the household?",
+ ""
+ ],
+ "labels": [
+ 19
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135887,
+ "endTime": 1768892345891,
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q22_4766403683020-1768317116985",
+ "question": [
+ "How many other people over the age of 18 are living in the household?",
+ ""
+ ],
+ "questionNumber": "9",
+ "reportType": "default"
+ },
+ "696660bce56b708ddc8dfbc7": {
+ "qid": "696660bce56b708ddc8dfbc7",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "18-24 years",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135893,
+ "endTime": 1768892356170,
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q23_4766403683020-1768317116985",
+ "question": [
+ "18-24 years",
+ ""
+ ],
+ "questionNumber": "10",
+ "reportType": "default"
+ },
+ "696660bce56b708ddc8dfbc8": {
+ "qid": "696660bce56b708ddc8dfbc8",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "25-34 years",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135897,
+ "endTime": 1768892357034,
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q24_4766403683020-1768317116986",
+ "question": [
+ "25-34 years",
+ ""
+ ],
+ "questionNumber": "11",
+ "reportType": "default"
+ },
+ "696660bce56b708ddc8dfbc9": {
+ "qid": "696660bce56b708ddc8dfbc9",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "35-39 years",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135910,
+ "endTime": 1768892357822,
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q25_4766403683020-1768317116987",
+ "question": [
+ "35-39 years",
+ ""
+ ],
+ "questionNumber": "12",
+ "reportType": "default"
+ },
+ "696660bce56b708ddc8dfbca": {
+ "qid": "696660bce56b708ddc8dfbca",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "60 or older",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135914,
+ "endTime": 1768892359002,
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q26_4766403683020-1768317116988",
+ "question": [
+ "60 or older",
+ ""
+ ],
+ "questionNumber": "13",
+ "reportType": "default"
+ },
+ "696660bce56b708ddc8dfbcb": {
+ "qid": "696660bce56b708ddc8dfbcb",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Never been to school",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135917,
+ "endTime": 1768892360178,
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q27_4766403683020-1768317116988",
+ "question": [
+ "Never been to school",
+ ""
+ ],
+ "questionNumber": "14",
+ "reportType": "default"
+ },
+ "696660bce56b708ddc8dfbcc": {
+ "qid": "696660bce56b708ddc8dfbcc",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Crèche/nursery school",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135921,
+ "endTime": 1768892360528,
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q28_4766403683020-1768317116989",
+ "question": [
+ "Crèche/nursery school",
+ ""
+ ],
+ "questionNumber": "15",
+ "reportType": "default"
+ },
+ "696660bce56b708ddc8dfbcd": {
+ "qid": "696660bce56b708ddc8dfbcd",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Some primary school",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135924,
+ "endTime": 1768892360787,
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q29_4766403683020-1768317116990",
+ "question": [
+ "Some primary school",
+ ""
+ ],
+ "questionNumber": "16",
+ "reportType": "default"
+ },
+ "696660bce56b708ddc8dfbce": {
+ "qid": "696660bce56b708ddc8dfbce",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Primary completed",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135927,
+ "endTime": 1768892361132,
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q30_4766403683020-1768317116991",
+ "question": [
+ "Primary completed",
+ ""
+ ],
+ "questionNumber": "17",
+ "reportType": "default"
+ },
+ "696660bce56b708ddc8dfbcf": {
+ "qid": "696660bce56b708ddc8dfbcf",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Some high school",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135929,
+ "endTime": 1768892361495,
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q31_4766403683020-1768317116991",
+ "question": [
+ "Some high school",
+ ""
+ ],
+ "questionNumber": "18",
+ "reportType": "default"
+ },
+ "696660bce56b708ddc8dfbd0": {
+ "qid": "696660bce56b708ddc8dfbd0",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "High school completed (Matric)",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135932,
+ "endTime": 1768892362105,
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q32_4766403683020-1768317116992",
+ "question": [
+ "High school completed (Matric)",
+ ""
+ ],
+ "questionNumber": "19",
+ "reportType": "default"
+ },
+ "696660bce56b708ddc8dfbd1": {
+ "qid": "696660bce56b708ddc8dfbd1",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "N. Qualification/Vocational training",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135935,
+ "endTime": 1768892362541,
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q33_4766403683020-1768317116994",
+ "question": [
+ "N. Qualification/Vocational training",
+ ""
+ ],
+ "questionNumber": "20",
+ "reportType": "default"
+ },
+ "696660bce56b708ddc8dfbd2": {
+ "qid": "696660bce56b708ddc8dfbd2",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Higher certificate",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135954,
+ "endTime": 1768892362828,
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q34_4766403683020-1768317116995",
+ "question": [
+ "Higher certificate",
+ ""
+ ],
+ "questionNumber": "21",
+ "reportType": "default"
+ },
+ "696660bce56b708ddc8dfbd3": {
+ "qid": "696660bce56b708ddc8dfbd3",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Diploma",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135959,
+ "endTime": 1768892363139,
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q35_4766403683020-1768317116995",
+ "question": [
+ "Diploma",
+ ""
+ ],
+ "questionNumber": "22",
+ "reportType": "default"
+ },
+ "696660bce56b708ddc8dfbd4": {
+ "qid": "696660bce56b708ddc8dfbd4",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "University degree",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135967,
+ "endTime": 1768892363487,
+ "criteriaId": "696660bde56b708ddc8dfc59",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q36_4766403683020-1768317116996",
+ "question": [
+ "University degree",
+ ""
+ ],
+ "questionNumber": "23",
+ "reportType": "default"
+ },
+ "696660bce56b708ddc8dfbd5": {
+ "qid": "696660bce56b708ddc8dfbd5",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "How many of each of these grants does your household receive?",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135974,
+ "endTime": 1768892231025,
+ "criteriaId": "696660bde56b708ddc8dfc5b",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q37_4766403683020-1768317116996",
+ "question": [
+ "How many of each of these grants does your household receive?",
+ ""
+ ],
+ "questionNumber": "1",
+ "reportType": "default"
+ },
+ "696660bce56b708ddc8dfbd6": {
+ "qid": "696660bce56b708ddc8dfbd6",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Child support grant",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135977,
+ "endTime": 1768892231983,
+ "criteriaId": "696660bde56b708ddc8dfc5b",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q38_4766403683020-1768317116997",
+ "question": [
+ "Child support grant",
+ ""
+ ],
+ "questionNumber": "2",
+ "reportType": "default"
+ },
+ "696660bce56b708ddc8dfbd7": {
+ "qid": "696660bce56b708ddc8dfbd7",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Child support grant top up",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135980,
+ "endTime": 1768892232653,
+ "criteriaId": "696660bde56b708ddc8dfc5b",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q39_4766403683020-1768317116998",
+ "question": [
+ "Child support grant top up",
+ ""
+ ],
+ "questionNumber": "3",
+ "reportType": "default"
+ },
+ "696660bce56b708ddc8dfbd8": {
+ "qid": "696660bce56b708ddc8dfbd8",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Foster care grant",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135984,
+ "endTime": 1768892233147,
+ "criteriaId": "696660bde56b708ddc8dfc5b",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q40_4766403683020-1768317116999",
+ "question": [
+ "Foster care grant",
+ ""
+ ],
+ "questionNumber": "4",
+ "reportType": "default"
+ },
+ "696660bce56b708ddc8dfbd9": {
+ "qid": "696660bce56b708ddc8dfbd9",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Government disability grant",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135989,
+ "endTime": 1768892233558,
+ "criteriaId": "696660bde56b708ddc8dfc5b",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q41_4766403683020-1768317116999",
+ "question": [
+ "Government disability grant",
+ ""
+ ],
+ "questionNumber": "5",
+ "reportType": "default"
+ },
+ "696660bde56b708ddc8dfbda": {
+ "qid": "696660bde56b708ddc8dfbda",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "War veterans grant",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135993,
+ "endTime": 1768892234216,
+ "criteriaId": "696660bde56b708ddc8dfc5b",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q42_4766403683020-1768317117000",
+ "question": [
+ "War veterans grant",
+ ""
+ ],
+ "questionNumber": "6",
+ "reportType": "default"
+ },
+ "696660bde56b708ddc8dfbdb": {
+ "qid": "696660bde56b708ddc8dfbdb",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Covid-19 social grant",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135996,
+ "endTime": 1768892235268,
+ "criteriaId": "696660bde56b708ddc8dfc5b",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q43_4766403683020-1768317117000",
+ "question": [
+ "Covid-19 social grant",
+ ""
+ ],
+ "questionNumber": "7",
+ "reportType": "default"
+ },
+ "696660bde56b708ddc8dfbdc": {
+ "qid": "696660bde56b708ddc8dfbdc",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Social Relief of Distress (SRD) grant",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886135999,
+ "endTime": 1768892235815,
+ "criteriaId": "696660bde56b708ddc8dfc5b",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q44_4766403683020-1768317117002",
+ "question": [
+ "Social Relief of Distress (SRD) grant",
+ ""
+ ],
+ "questionNumber": "8",
+ "reportType": "default"
+ },
+ "696660bde56b708ddc8dfbdd": {
+ "qid": "696660bde56b708ddc8dfbdd",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Grant for older persons",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136001,
+ "endTime": 1768892236165,
+ "criteriaId": "696660bde56b708ddc8dfc5b",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q46_4766403683020-1768317117003",
+ "question": [
+ "Grant for older persons",
+ ""
+ ],
+ "questionNumber": "9",
+ "reportType": "default"
+ },
+ "696660bde56b708ddc8dfbde": {
+ "qid": "696660bde56b708ddc8dfbde",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Care dependency grant",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136004,
+ "endTime": 1768892236497,
+ "criteriaId": "696660bde56b708ddc8dfc5b",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q47_4766403683020-1768317117004",
+ "question": [
+ "Care dependency grant",
+ ""
+ ],
+ "questionNumber": "10",
+ "reportType": "default"
+ },
+ "696660bde56b708ddc8dfbdf": {
+ "qid": "696660bde56b708ddc8dfbdf",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Grant-in-Aid",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136007,
+ "endTime": 1768892236827,
+ "criteriaId": "696660bde56b708ddc8dfc5b",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q48_4766403683020-1768317117004",
+ "question": [
+ "Grant-in-Aid",
+ ""
+ ],
+ "questionNumber": "11",
+ "reportType": "default"
+ },
+ "696660bde56b708ddc8dfbe0": {
+ "qid": "696660bde56b708ddc8dfbe0",
+ "value": [
+ "R2"
+ ],
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Now, which of these grants do YOU receive?",
+ ""
+ ],
+ "labels": [
+ "R2"
+ ],
+ "responseType": "multiselect-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136009,
+ "endTime": 1768892240771,
+ "criteriaId": "696660bde56b708ddc8dfc5b",
+ "responseType": "multiselect-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [
+ {
+ "value": "R1",
+ "label": "Child support grant"
+ },
+ {
+ "value": "R2",
+ "label": "Child support grant top up"
+ },
+ {
+ "value": "R3",
+ "label": "Foster care grant"
+ },
+ {
+ "value": "R4",
+ "label": "Government disability grant"
+ },
+ {
+ "value": "R5",
+ "label": "War veterans grant"
+ },
+ {
+ "value": "R6",
+ "label": "Covid-19 social grant"
+ },
+ {
+ "value": "R7",
+ "label": "Social Relief of Distress (SRD) grant"
+ },
+ {
+ "value": "R8",
+ "label": "Grant for older persons"
+ },
+ {
+ "value": "R9",
+ "label": "Care dependency grant"
+ },
+ {
+ "value": "R10",
+ "label": "Grant-in-Aid"
+ }
+ ],
+ "externalId": "Q49_4766403683020-1768317117005",
+ "question": [
+ "Now, which of these grants do YOU receive?",
+ ""
+ ],
+ "questionNumber": "11",
+ "reportType": "default"
+ },
+ "696660bde56b708ddc8dfbe1": {
+ "qid": "696660bde56b708ddc8dfbe1",
+ "value": "R2",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Unemployment insurance fund/UIF/UIF TERS Covid Benefits",
+ ""
+ ],
+ "labels": [
+ "R2"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136021,
+ "endTime": 1768892243834,
+ "criteriaId": "696660bde56b708ddc8dfc5d",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [
+ {
+ "value": "R1",
+ "label": "R0 - R1000"
+ },
+ {
+ "value": "R2",
+ "label": "R1001 – R3000"
+ },
+ {
+ "value": "R3",
+ "label": "R3001 - R5000"
+ },
+ {
+ "value": "R4",
+ "label": "R5001 - R7000"
+ },
+ {
+ "value": "R5",
+ "label": "R7001 – R9000"
+ },
+ {
+ "value": "R6",
+ "label": "R9001 – above"
+ }
+ ],
+ "externalId": "Q50_4766403683020-1768317117006",
+ "question": [
+ "Unemployment insurance fund/UIF/UIF TERS Covid Benefits",
+ ""
+ ],
+ "questionNumber": "1",
+ "reportType": "default"
+ },
+ "696660bde56b708ddc8dfbe2": {
+ "qid": "696660bde56b708ddc8dfbe2",
+ "value": "R2",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Income from household business",
+ ""
+ ],
+ "labels": [
+ "R2"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136027,
+ "endTime": 1768892246041,
+ "criteriaId": "696660bde56b708ddc8dfc5d",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [
+ {
+ "value": "R1",
+ "label": "R0 - R1000"
+ },
+ {
+ "value": "R2",
+ "label": "R1001 – R3000"
+ },
+ {
+ "value": "R3",
+ "label": "R3001 - R5000"
+ },
+ {
+ "value": "R4",
+ "label": "R5001 - R7000"
+ },
+ {
+ "value": "R5",
+ "label": "R7001 – R9000"
+ },
+ {
+ "value": "R6",
+ "label": "R9001 – above"
+ }
+ ],
+ "externalId": "Q51_4766403683020-1768317117006",
+ "question": [
+ "Income from household business",
+ ""
+ ],
+ "questionNumber": "2",
+ "reportType": "default"
+ },
+ "696660bde56b708ddc8dfbe3": {
+ "qid": "696660bde56b708ddc8dfbe3",
+ "value": "R2",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Salary/wages from PUBLIC SECTOR job",
+ ""
+ ],
+ "labels": [
+ "R2"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136033,
+ "endTime": 1768892247941,
+ "criteriaId": "696660bde56b708ddc8dfc5d",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [
+ {
+ "value": "R1",
+ "label": "R0 - R1000"
+ },
+ {
+ "value": "R2",
+ "label": "R1001 – R3000"
+ },
+ {
+ "value": "R3",
+ "label": "R3001 - R5000"
+ },
+ {
+ "value": "R4",
+ "label": "R5001 - R7000"
+ },
+ {
+ "value": "R5",
+ "label": "R7001 – R9000"
+ },
+ {
+ "value": "R6",
+ "label": "R9001 – above"
+ }
+ ],
+ "externalId": "Q52_4766403683020-1768317117008",
+ "question": [
+ "Salary/wages from PUBLIC SECTOR job",
+ ""
+ ],
+ "questionNumber": "3",
+ "reportType": "default"
+ },
+ "696660bde56b708ddc8dfbe4": {
+ "qid": "696660bde56b708ddc8dfbe4",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Salary/wages from PRIVATE SECTOR job",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136041,
+ "endTime": 1768892249559,
+ "criteriaId": "696660bde56b708ddc8dfc5d",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [
+ {
+ "value": "R1",
+ "label": "R0 - R1000"
+ },
+ {
+ "value": "R2",
+ "label": "R1001 – R3000"
+ },
+ {
+ "value": "R3",
+ "label": "R3001 - R5000"
+ },
+ {
+ "value": "R4",
+ "label": "R5001 - R7000"
+ },
+ {
+ "value": "R5",
+ "label": "R7001 – R9000"
+ },
+ {
+ "value": "R6",
+ "label": "R9001 – above"
+ }
+ ],
+ "externalId": "Q53_4766403683020-1768317117009",
+ "question": [
+ "Salary/wages from PRIVATE SECTOR job",
+ ""
+ ],
+ "questionNumber": "4",
+ "reportType": "default"
+ },
+ "696660bde56b708ddc8dfbe5": {
+ "qid": "696660bde56b708ddc8dfbe5",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Salary/wage from individuals (domestic worker, gardener)",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136051,
+ "endTime": 1768892250456,
+ "criteriaId": "696660bde56b708ddc8dfc5d",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [
+ {
+ "value": "R1",
+ "label": "R0 - R1000"
+ },
+ {
+ "value": "R2",
+ "label": "R1001 – R3000"
+ },
+ {
+ "value": "R3",
+ "label": "R3001 - R5000"
+ },
+ {
+ "value": "R4",
+ "label": "R5001 - R7000"
+ },
+ {
+ "value": "R5",
+ "label": "R7001 – R9000"
+ },
+ {
+ "value": "R6",
+ "label": "R9001 – above"
+ }
+ ],
+ "externalId": "Q54_4766403683020-1768317117010",
+ "question": [
+ "Salary/wage from individuals (domestic worker, gardener)",
+ ""
+ ],
+ "questionNumber": "5",
+ "reportType": "default"
+ },
+ "696660bde56b708ddc8dfbe6": {
+ "qid": "696660bde56b708ddc8dfbe6",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Money from rent you receive",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136063,
+ "endTime": 1768892251059,
+ "criteriaId": "696660bde56b708ddc8dfc5d",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [
+ {
+ "value": "R1",
+ "label": "R0 - R1000"
+ },
+ {
+ "value": "R2",
+ "label": "R1001 – R3000"
+ },
+ {
+ "value": "R3",
+ "label": "R3001 - R5000"
+ },
+ {
+ "value": "R4",
+ "label": "R5001 - R7000"
+ },
+ {
+ "value": "R5",
+ "label": "R7001 – R9000"
+ },
+ {
+ "value": "R6",
+ "label": "R9001 – above"
+ }
+ ],
+ "externalId": "Q55_4766403683020-1768317117010",
+ "question": [
+ "Money from rent you receive",
+ ""
+ ],
+ "questionNumber": "6",
+ "reportType": "default"
+ },
+ "696660bde56b708ddc8dfbe7": {
+ "qid": "696660bde56b708ddc8dfbe7",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Piece job (fixed piece rate for task)",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136071,
+ "endTime": 1768892251580,
+ "criteriaId": "696660bde56b708ddc8dfc5d",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [
+ {
+ "value": "R1",
+ "label": "R0 - R1000"
+ },
+ {
+ "value": "R2",
+ "label": "R1001 – R3000"
+ },
+ {
+ "value": "R3",
+ "label": "R3001 - R5000"
+ },
+ {
+ "value": "R4",
+ "label": "R5001 - R7000"
+ },
+ {
+ "value": "R5",
+ "label": "R7001 – R9000"
+ },
+ {
+ "value": "R6",
+ "label": "R9001 – above"
+ }
+ ],
+ "externalId": "Q56_4766403683020-1768317117011",
+ "question": [
+ "Piece job (fixed piece rate for task)",
+ ""
+ ],
+ "questionNumber": "7",
+ "reportType": "default"
+ },
+ "696660bde56b708ddc8dfbe8": {
+ "qid": "696660bde56b708ddc8dfbe8",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Work pension or provident fund",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136078,
+ "endTime": 1768892252293,
+ "criteriaId": "696660bde56b708ddc8dfc5d",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [
+ {
+ "value": "R1",
+ "label": "R0 - R1000"
+ },
+ {
+ "value": "R2",
+ "label": "R1001 – R3000"
+ },
+ {
+ "value": "R3",
+ "label": "R3001 - R5000"
+ },
+ {
+ "value": "R4",
+ "label": "R5001 - R7000"
+ },
+ {
+ "value": "R5",
+ "label": "R7001 – R9000"
+ },
+ {
+ "value": "R6",
+ "label": "R9001 – above"
+ }
+ ],
+ "externalId": "Q57_4766403683020-1768317117012",
+ "question": [
+ "Work pension or provident fund",
+ ""
+ ],
+ "questionNumber": "8",
+ "reportType": "default"
+ },
+ "696660bde56b708ddc8dfbe9": {
+ "qid": "696660bde56b708ddc8dfbe9",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Money from friend or family member",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136087,
+ "endTime": 1768892254554,
+ "criteriaId": "696660bde56b708ddc8dfc5d",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [
+ {
+ "value": "R1",
+ "label": "R0 - R1000"
+ },
+ {
+ "value": "R2",
+ "label": "R1001 – R3000"
+ },
+ {
+ "value": "R3",
+ "label": "R3001 - R5000"
+ },
+ {
+ "value": "R4",
+ "label": "R5001 - R7000"
+ },
+ {
+ "value": "R5",
+ "label": "R7001 – R9000"
+ },
+ {
+ "value": "R6",
+ "label": "R9001 – above"
+ }
+ ],
+ "externalId": "Q58_4766403683020-1768317117012",
+ "question": [
+ "Money from friend or family member",
+ ""
+ ],
+ "questionNumber": "9",
+ "reportType": "default"
+ },
+ "696660bde56b708ddc8dfbea": {
+ "qid": "696660bde56b708ddc8dfbea",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Money from maintenance from former spouse/partner",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136094,
+ "endTime": 1768892255359,
+ "criteriaId": "696660bde56b708ddc8dfc5d",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [
+ {
+ "value": "R1",
+ "label": "R0 - R1000"
+ },
+ {
+ "value": "R2",
+ "label": "R1001 – R3000"
+ },
+ {
+ "value": "R3",
+ "label": "R3001 - R5000"
+ },
+ {
+ "value": "R4",
+ "label": "R5001 - R7000"
+ },
+ {
+ "value": "R5",
+ "label": "R7001 – R9000"
+ },
+ {
+ "value": "R6",
+ "label": "R9001 – above"
+ }
+ ],
+ "externalId": "Q59_4766403683020-1768317117013",
+ "question": [
+ "Money from maintenance from former spouse/partner",
+ ""
+ ],
+ "questionNumber": "10",
+ "reportType": "default"
+ },
+ "696660bde56b708ddc8dfbeb": {
+ "qid": "696660bde56b708ddc8dfbeb",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Other income source",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136102,
+ "endTime": 1768892255927,
+ "criteriaId": "696660bde56b708ddc8dfc5d",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [
+ {
+ "value": "R1",
+ "label": "R0 - R1000"
+ },
+ {
+ "value": "R2",
+ "label": "R1001 – R3000"
+ },
+ {
+ "value": "R3",
+ "label": "R3001 - R5000"
+ },
+ {
+ "value": "R4",
+ "label": "R5001 - R7000"
+ },
+ {
+ "value": "R5",
+ "label": "R7001 – R9000"
+ },
+ {
+ "value": "R6",
+ "label": "R9001 – above"
+ }
+ ],
+ "externalId": "Q60_4766403683020-1768317117013",
+ "question": [
+ "Other income source",
+ ""
+ ],
+ "questionNumber": "11",
+ "reportType": "default"
+ },
+ "696660bde56b708ddc8dfbec": {
+ "qid": "696660bde56b708ddc8dfbec",
+ "value": "hello",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Please specify other income source",
+ ""
+ ],
+ "labels": [
+ "hello"
+ ],
+ "responseType": "text",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136111,
+ "endTime": 1768892261589,
+ "criteriaId": "696660bde56b708ddc8dfc5d",
+ "responseType": "text",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q61_4766403683020-1768317117014",
+ "question": [
+ "Please specify other income source",
+ ""
+ ],
+ "questionNumber": "12",
+ "reportType": "default"
+ },
+ "696660bde56b708ddc8dfbed": {
+ "qid": "696660bde56b708ddc8dfbed",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Unemployment insurance fund/UIF/UIF TERS Covid Benefits",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136116,
+ "endTime": 1768892262794,
+ "criteriaId": "696660bde56b708ddc8dfc5f",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [
+ {
+ "value": "R1",
+ "label": "R0 - R1000"
+ },
+ {
+ "value": "R2",
+ "label": "R1001 – R3000"
+ },
+ {
+ "value": "R3",
+ "label": "R3001 - R5000"
+ },
+ {
+ "value": "R4",
+ "label": "R5001 - R7000"
+ },
+ {
+ "value": "R5",
+ "label": "R7001 – R9000"
+ },
+ {
+ "value": "R6",
+ "label": "R9001 – above"
+ }
+ ],
+ "externalId": "Q62_4766403683020-1768317117015",
+ "question": [
+ "Unemployment insurance fund/UIF/UIF TERS Covid Benefits",
+ ""
+ ],
+ "questionNumber": "13",
+ "reportType": "default"
+ },
+ "696660bde56b708ddc8dfbee": {
+ "qid": "696660bde56b708ddc8dfbee",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Income from my own business",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136126,
+ "endTime": 1768892263361,
+ "criteriaId": "696660bde56b708ddc8dfc5f",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [
+ {
+ "value": "R1",
+ "label": "R0 - R1000"
+ },
+ {
+ "value": "R2",
+ "label": "R1001 – R3000"
+ },
+ {
+ "value": "R3",
+ "label": "R3001 - R5000"
+ },
+ {
+ "value": "R4",
+ "label": "R5001 - R7000"
+ },
+ {
+ "value": "R5",
+ "label": "R7001 – R9000"
+ },
+ {
+ "value": "R6",
+ "label": "R9001 – above"
+ }
+ ],
+ "externalId": "Q63_4766403683020-1768317117015",
+ "question": [
+ "Income from my own business",
+ ""
+ ],
+ "questionNumber": "14",
+ "reportType": "default"
+ },
+ "696660bde56b708ddc8dfbef": {
+ "qid": "696660bde56b708ddc8dfbef",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Salary/wages from PUBLIC SECTOR job",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136138,
+ "endTime": 1768892263920,
+ "criteriaId": "696660bde56b708ddc8dfc5f",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [
+ {
+ "value": "R1",
+ "label": "R0 - R1000"
+ },
+ {
+ "value": "R2",
+ "label": "R1001 – R3000"
+ },
+ {
+ "value": "R3",
+ "label": "R3001 - R5000"
+ },
+ {
+ "value": "R4",
+ "label": "R5001 - R7000"
+ },
+ {
+ "value": "R5",
+ "label": "R7001 – R9000"
+ },
+ {
+ "value": "R6",
+ "label": "R9001 – above"
+ }
+ ],
+ "externalId": "Q64_4766403683020-1768317117016",
+ "question": [
+ "Salary/wages from PUBLIC SECTOR job",
+ ""
+ ],
+ "questionNumber": "15",
+ "reportType": "default"
+ },
+ "696660bde56b708ddc8dfbf0": {
+ "qid": "696660bde56b708ddc8dfbf0",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Salary/wages from PRIVATE SECTOR job",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136146,
+ "endTime": 1768892264477,
+ "criteriaId": "696660bde56b708ddc8dfc5f",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [
+ {
+ "value": "R1",
+ "label": "R0 - R1000"
+ },
+ {
+ "value": "R2",
+ "label": "R1001 – R3000"
+ },
+ {
+ "value": "R3",
+ "label": "R3001 - R5000"
+ },
+ {
+ "value": "R4",
+ "label": "R5001 - R7000"
+ },
+ {
+ "value": "R5",
+ "label": "R7001 – R9000"
+ },
+ {
+ "value": "R6",
+ "label": "R9001 – above"
+ }
+ ],
+ "externalId": "Q65_4766403683020-1768317117016",
+ "question": [
+ "Salary/wages from PRIVATE SECTOR job",
+ ""
+ ],
+ "questionNumber": "16",
+ "reportType": "default"
+ },
+ "696660bde56b708ddc8dfbf1": {
+ "qid": "696660bde56b708ddc8dfbf1",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Salary/wage from individuals (domestic worker, gardener)",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136152,
+ "endTime": 1768892264949,
+ "criteriaId": "696660bde56b708ddc8dfc5f",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [
+ {
+ "value": "R1",
+ "label": "R0 - R1000"
+ },
+ {
+ "value": "R2",
+ "label": "R1001 – R3000"
+ },
+ {
+ "value": "R3",
+ "label": "R3001 - R5000"
+ },
+ {
+ "value": "R4",
+ "label": "R5001 - R7000"
+ },
+ {
+ "value": "R5",
+ "label": "R7001 – R9000"
+ },
+ {
+ "value": "R6",
+ "label": "R9001 – above"
+ }
+ ],
+ "externalId": "Q66_4766403683020-1768317117017",
+ "question": [
+ "Salary/wage from individuals (domestic worker, gardener)",
+ ""
+ ],
+ "questionNumber": "17",
+ "reportType": "default"
+ },
+ "696660bde56b708ddc8dfbf2": {
+ "qid": "696660bde56b708ddc8dfbf2",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Money from rent you receive",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136159,
+ "endTime": 1768892265483,
+ "criteriaId": "696660bde56b708ddc8dfc5f",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [
+ {
+ "value": "R1",
+ "label": "R0 - R1000"
+ },
+ {
+ "value": "R2",
+ "label": "R1001 – R3000"
+ },
+ {
+ "value": "R3",
+ "label": "R3001 - R5000"
+ },
+ {
+ "value": "R4",
+ "label": "R5001 - R7000"
+ },
+ {
+ "value": "R5",
+ "label": "R7001 – R9000"
+ },
+ {
+ "value": "R6",
+ "label": "R9001 – above"
+ }
+ ],
+ "externalId": "Q67_4766403683020-1768317117018",
+ "question": [
+ "Money from rent you receive",
+ ""
+ ],
+ "questionNumber": "18",
+ "reportType": "default"
+ },
+ "696660bde56b708ddc8dfbf3": {
+ "qid": "696660bde56b708ddc8dfbf3",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Piece job (fixed piece rate for task)",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136169,
+ "endTime": 1768892266078,
+ "criteriaId": "696660bde56b708ddc8dfc5f",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [
+ {
+ "value": "R1",
+ "label": "R0 - R1000"
+ },
+ {
+ "value": "R2",
+ "label": "R1001 – R3000"
+ },
+ {
+ "value": "R3",
+ "label": "R3001 - R5000"
+ },
+ {
+ "value": "R4",
+ "label": "R5001 - R7000"
+ },
+ {
+ "value": "R5",
+ "label": "R7001 – R9000"
+ },
+ {
+ "value": "R6",
+ "label": "R9001 – above"
+ }
+ ],
+ "externalId": "Q68_4766403683020-1768317117018",
+ "question": [
+ "Piece job (fixed piece rate for task)",
+ ""
+ ],
+ "questionNumber": "19",
+ "reportType": "default"
+ },
+ "696660bde56b708ddc8dfbf4": {
+ "qid": "696660bde56b708ddc8dfbf4",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Work pension or provident fund",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136177,
+ "endTime": 1768892266577,
+ "criteriaId": "696660bde56b708ddc8dfc5f",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [
+ {
+ "value": "R1",
+ "label": "R0 - R1000"
+ },
+ {
+ "value": "R2",
+ "label": "R1001 – R3000"
+ },
+ {
+ "value": "R3",
+ "label": "R3001 - R5000"
+ },
+ {
+ "value": "R4",
+ "label": "R5001 - R7000"
+ },
+ {
+ "value": "R5",
+ "label": "R7001 – R9000"
+ },
+ {
+ "value": "R6",
+ "label": "R9001 – above"
+ }
+ ],
+ "externalId": "Q69_4766403683020-1768317117019",
+ "question": [
+ "Work pension or provident fund",
+ ""
+ ],
+ "questionNumber": "20",
+ "reportType": "default"
+ },
+ "696660bde56b708ddc8dfbf5": {
+ "qid": "696660bde56b708ddc8dfbf5",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Money from friend or family member",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136185,
+ "endTime": 1768892267094,
+ "criteriaId": "696660bde56b708ddc8dfc5f",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [
+ {
+ "value": "R1",
+ "label": "R0 - R1000"
+ },
+ {
+ "value": "R2",
+ "label": "R1001 – R3000"
+ },
+ {
+ "value": "R3",
+ "label": "R3001 - R5000"
+ },
+ {
+ "value": "R4",
+ "label": "R5001 - R7000"
+ },
+ {
+ "value": "R5",
+ "label": "R7001 – R9000"
+ },
+ {
+ "value": "R6",
+ "label": "R9001 – above"
+ }
+ ],
+ "externalId": "Q70_4766403683020-1768317117020",
+ "question": [
+ "Money from friend or family member",
+ ""
+ ],
+ "questionNumber": "21",
+ "reportType": "default"
+ },
+ "696660bde56b708ddc8dfbf6": {
+ "qid": "696660bde56b708ddc8dfbf6",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Money from maintenance from former spouse/partner",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136192,
+ "endTime": 1768892267580,
+ "criteriaId": "696660bde56b708ddc8dfc5f",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [
+ {
+ "value": "R1",
+ "label": "R0 - R1000"
+ },
+ {
+ "value": "R2",
+ "label": "R1001 – R3000"
+ },
+ {
+ "value": "R3",
+ "label": "R3001 - R5000"
+ },
+ {
+ "value": "R4",
+ "label": "R5001 - R7000"
+ },
+ {
+ "value": "R5",
+ "label": "R7001 – R9000"
+ },
+ {
+ "value": "R6",
+ "label": "R9001 – above"
+ }
+ ],
+ "externalId": "Q71_4766403683020-1768317117020",
+ "question": [
+ "Money from maintenance from former spouse/partner",
+ ""
+ ],
+ "questionNumber": "22",
+ "reportType": "default"
+ },
+ "696660bde56b708ddc8dfbf7": {
+ "qid": "696660bde56b708ddc8dfbf7",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Other income source",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136201,
+ "endTime": 1768892269177,
+ "criteriaId": "696660bde56b708ddc8dfc5f",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [
+ {
+ "value": "R1",
+ "label": "R0 - R1000"
+ },
+ {
+ "value": "R2",
+ "label": "R1001 – R3000"
+ },
+ {
+ "value": "R3",
+ "label": "R3001 - R5000"
+ },
+ {
+ "value": "R4",
+ "label": "R5001 - R7000"
+ },
+ {
+ "value": "R5",
+ "label": "R7001 – R9000"
+ },
+ {
+ "value": "R6",
+ "label": "R9001 – above"
+ }
+ ],
+ "externalId": "Q72_4766403683020-1768317117021",
+ "question": [
+ "Other income source",
+ ""
+ ],
+ "questionNumber": "23",
+ "reportType": "default"
+ },
+ "696660bde56b708ddc8dfbf8": {
+ "qid": "696660bde56b708ddc8dfbf8",
+ "value": "hello",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Please specify other income source",
+ ""
+ ],
+ "labels": [
+ "hello"
+ ],
+ "responseType": "text",
+ "filesNotUploaded": []
+ },
+ "startTime": 1768886136214,
+ "endTime": 1768892271723,
+ "criteriaId": "696660bde56b708ddc8dfc5f",
+ "responseType": "text",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "options": [],
+ "externalId": "Q73_4766403683020-1768317117022",
+ "question": [
+ "Please specify other income source",
+ ""
+ ],
+ "questionNumber": "24",
+ "reportType": "default"
+ },
+ "696660bde56b708ddc8dfbfa": {
+ "qid": "696660bde56b708ddc8dfbfa",
+ "gpsLocation": "",
+ "startTime": "",
+ "endTime": 1768892281734,
+ "criteriaId": "696660bde56b708ddc8dfc61",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "instanceResponses": [
+ "R1"
+ ],
+ "instanceRemarks": [
+ ""
+ ],
+ "instanceFileName": [
+ []
+ ],
+ "options": [
+ {
+ "value": "R1",
+ "label": "Productive tools and equipment"
+ },
+ {
+ "value": "R2",
+ "label": "Land"
+ },
+ {
+ "value": "R3",
+ "label": "Cattle"
+ },
+ {
+ "value": "R4",
+ "label": "Horses"
+ },
+ {
+ "value": "R5",
+ "label": "Goats"
+ },
+ {
+ "value": "R6",
+ "label": "Sheep"
+ },
+ {
+ "value": "R7",
+ "label": "Chicken/duck/poultry"
+ },
+ {
+ "value": "R8",
+ "label": "Cell phone (smartphone)"
+ },
+ {
+ "value": "R9",
+ "label": "Cell phone (old)"
+ },
+ {
+ "value": "R10",
+ "label": "Radio"
+ },
+ {
+ "value": "R11",
+ "label": "Television"
+ },
+ {
+ "value": "R12",
+ "label": "Computer"
+ },
+ {
+ "value": "R13",
+ "label": "Other"
+ }
+ ],
+ "externalId": "Q75_4766403683020-1768317117023",
+ "question": [
+ "Asset Type",
+ ""
+ ],
+ "questionNumber": "26",
+ "reportType": "default"
+ },
+ "696660bde56b708ddc8dfbfb": {
+ "qid": "696660bde56b708ddc8dfbfb",
+ "gpsLocation": "",
+ "startTime": "",
+ "endTime": "",
+ "criteriaId": "696660bde56b708ddc8dfc61",
+ "responseType": "text",
+ "evidenceMethod": "OB",
+ "visibleIf": [
+ {
+ "operator": "===",
+ "value": "R13",
+ "_id": "696660bde56b708ddc8dfbfa"
+ }
+ ],
+ "rubricLevel": "",
+ "instanceResponses": [
+ null
+ ],
+ "instanceRemarks": [
+ ""
+ ],
+ "instanceFileName": [
+ []
+ ],
+ "options": [],
+ "externalId": "Q76_4766403683020-1768317117024",
+ "question": [
+ "Specify Other Asset",
+ ""
+ ],
+ "questionNumber": "27",
+ "reportType": "default"
+ },
+ "696660bde56b708ddc8dfbfc": {
+ "qid": "696660bde56b708ddc8dfbfc",
+ "gpsLocation": "",
+ "startTime": "",
+ "endTime": 1768892283774,
+ "criteriaId": "696660bde56b708ddc8dfc61",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "instanceResponses": [
+ 1
+ ],
+ "instanceRemarks": [
+ ""
+ ],
+ "instanceFileName": [
+ []
+ ],
+ "options": [],
+ "externalId": "Q77_4766403683020-1768317117025",
+ "question": [
+ "Quantity owned by household",
+ ""
+ ],
+ "questionNumber": "28",
+ "reportType": "default"
+ },
+ "696660bde56b708ddc8dfbfd": {
+ "qid": "696660bde56b708ddc8dfbfd",
+ "gpsLocation": "",
+ "startTime": "",
+ "endTime": 1768892285684,
+ "criteriaId": "696660bde56b708ddc8dfc61",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "instanceResponses": [
+ 1
+ ],
+ "instanceRemarks": [
+ ""
+ ],
+ "instanceFileName": [
+ []
+ ],
+ "options": [],
+ "externalId": "Q78_4766403683020-1768317117026",
+ "question": [
+ "Quantity owned by you",
+ ""
+ ],
+ "questionNumber": "29",
+ "reportType": "default"
+ },
+ "696660bde56b708ddc8dfbfe": {
+ "qid": "696660bde56b708ddc8dfbfe",
+ "gpsLocation": "",
+ "startTime": "",
+ "endTime": 1768892288414,
+ "criteriaId": "696660bde56b708ddc8dfc61",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "instanceResponses": [
+ 1
+ ],
+ "instanceRemarks": [
+ ""
+ ],
+ "instanceFileName": [
+ []
+ ],
+ "options": [],
+ "externalId": "Q79_4766403683020-1768317117026",
+ "question": [
+ "Value per unit (R)",
+ ""
+ ],
+ "questionNumber": "30",
+ "reportType": "default"
+ },
+ "696660bde56b708ddc8dfbf9": {
+ "qid": "696660bde56b708ddc8dfbf9",
+ "value": [
+ {
+ "696660bde56b708ddc8dfbfa": {
+ "qid": "696660bde56b708ddc8dfbfa",
+ "value": "R1",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Asset Type",
+ ""
+ ],
+ "labels": [
+ "R1"
+ ],
+ "responseType": "select-dropdown",
+ "filesNotUploaded": []
+ },
+ "startTime": "",
+ "endTime": 1768892281734,
+ "criteriaId": "696660bde56b708ddc8dfc61",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbfb": {
+ "qid": "696660bde56b708ddc8dfbfb",
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Specify Other Asset",
+ ""
+ ],
+ "responseType": "text",
+ "filesNotUploaded": []
+ },
+ "startTime": "",
+ "endTime": "",
+ "criteriaId": "696660bde56b708ddc8dfc61",
+ "responseType": "text",
+ "evidenceMethod": "OB",
+ "visibleIf": [
+ {
+ "operator": "===",
+ "value": "R13",
+ "_id": "696660bde56b708ddc8dfbfa"
+ }
+ ],
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbfc": {
+ "qid": "696660bde56b708ddc8dfbfc",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Quantity owned by household",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": "",
+ "endTime": 1768892283774,
+ "criteriaId": "696660bde56b708ddc8dfc61",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbfd": {
+ "qid": "696660bde56b708ddc8dfbfd",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Quantity owned by you",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": "",
+ "endTime": 1768892285684,
+ "criteriaId": "696660bde56b708ddc8dfc61",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ },
+ "696660bde56b708ddc8dfbfe": {
+ "qid": "696660bde56b708ddc8dfbfe",
+ "value": 1,
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Value per unit (R)",
+ ""
+ ],
+ "labels": [
+ 1
+ ],
+ "responseType": "number",
+ "filesNotUploaded": []
+ },
+ "startTime": "",
+ "endTime": 1768892288414,
+ "criteriaId": "696660bde56b708ddc8dfc61",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": ""
+ }
+ }
+ ],
+ "remarks": "",
+ "fileName": [],
+ "gpsLocation": "",
+ "payload": {
+ "question": [
+ "Asset Inventory",
+ ""
+ ],
+ "labels": [
+ [
+ {
+ "_id": "696660bde56b708ddc8dfbfa",
+ "externalId": "Q75_4766403683020-1768317117023",
+ "question": [
+ "Asset Type",
+ ""
+ ],
+ "tip": "",
+ "hint": "",
+ "responseType": "select-dropdown",
+ "value": "R1",
+ "isCompleted": false,
+ "showRemarks": false,
+ "remarks": "",
+ "visibleIf": "",
+ "options": [
+ {
+ "value": "R1",
+ "label": "Productive tools and equipment"
+ },
+ {
+ "value": "R2",
+ "label": "Land"
+ },
+ {
+ "value": "R3",
+ "label": "Cattle"
+ },
+ {
+ "value": "R4",
+ "label": "Horses"
+ },
+ {
+ "value": "R5",
+ "label": "Goats"
+ },
+ {
+ "value": "R6",
+ "label": "Sheep"
+ },
+ {
+ "value": "R7",
+ "label": "Chicken/duck/poultry"
+ },
+ {
+ "value": "R8",
+ "label": "Cell phone (smartphone)"
+ },
+ {
+ "value": "R9",
+ "label": "Cell phone (old)"
+ },
+ {
+ "value": "R10",
+ "label": "Radio"
+ },
+ {
+ "value": "R11",
+ "label": "Television"
+ },
+ {
+ "value": "R12",
+ "label": "Computer"
+ },
+ {
+ "value": "R13",
+ "label": "Other"
+ }
+ ],
+ "sliderOptions": [],
+ "children": [
+ "696660bde56b708ddc8dfbfb"
+ ],
+ "questionGroup": [
+ "A1"
+ ],
+ "questionType": "auto",
+ "modeOfCollection": "",
+ "usedForScoring": "",
+ "fileName": [],
+ "validation": {
+ "required": false
+ },
+ "accessibility": "",
+ "canBeNotApplicable": "false",
+ "instanceQuestions": [],
+ "isAGeneralQuestion": false,
+ "autoCapture": false,
+ "rubricLevel": "",
+ "sectionHeader": "",
+ "allowAudioRecording": false,
+ "page": "p4",
+ "questionNumber": "",
+ "prefillFromEntityProfile": false,
+ "entityFieldName": "",
+ "isEditable": true,
+ "showQuestionInPreview": false,
+ "createdFromQuestionId": "69666080e56b708ddc8dfb89",
+ "orgId": "brac_gbl",
+ "tenantId": "brac",
+ "reportType": "default",
+ "updatedAt": "2026-01-13T15:11:57.036Z",
+ "createdAt": "2026-01-13T15:10:56.812Z",
+ "deleted": false,
+ "__v": 0,
+ "evidenceMethod": "OB",
+ "payload": {
+ "criteriaId": "696660bde56b708ddc8dfc61",
+ "responseType": "select-dropdown",
+ "evidenceMethod": "OB",
+ "rubricLevel": ""
+ },
+ "startTime": "",
+ "endTime": 1768892281734,
+ "gpsLocation": "",
+ "file": ""
+ },
+ {
+ "_id": "696660bde56b708ddc8dfbfb",
+ "externalId": "Q76_4766403683020-1768317117024",
+ "question": [
+ "Specify Other Asset",
+ ""
+ ],
+ "tip": "",
+ "hint": "",
+ "responseType": "text",
+ "isCompleted": false,
+ "showRemarks": false,
+ "remarks": "",
+ "visibleIf": [
+ {
+ "operator": "===",
+ "value": "R13",
+ "_id": "696660bde56b708ddc8dfbfa"
+ }
+ ],
+ "options": [],
+ "sliderOptions": [],
+ "children": [],
+ "questionGroup": [
+ "A1"
+ ],
+ "questionType": "auto",
+ "modeOfCollection": "",
+ "usedForScoring": "",
+ "fileName": [],
+ "validation": {
+ "required": false
+ },
+ "accessibility": "",
+ "canBeNotApplicable": "false",
+ "instanceQuestions": [],
+ "isAGeneralQuestion": false,
+ "autoCapture": false,
+ "rubricLevel": "",
+ "sectionHeader": "",
+ "allowAudioRecording": false,
+ "page": "p4",
+ "questionNumber": "",
+ "prefillFromEntityProfile": false,
+ "entityFieldName": "",
+ "isEditable": true,
+ "showQuestionInPreview": false,
+ "createdFromQuestionId": "69666080e56b708ddc8dfb90",
+ "orgId": "brac_gbl",
+ "tenantId": "brac",
+ "reportType": "default",
+ "updatedAt": "2026-01-13T15:11:57.036Z",
+ "createdAt": "2026-01-13T15:10:56.831Z",
+ "deleted": false,
+ "__v": 0,
+ "evidenceMethod": "OB",
+ "payload": {
+ "criteriaId": "696660bde56b708ddc8dfc61",
+ "responseType": "text",
+ "evidenceMethod": "OB",
+ "rubricLevel": ""
+ },
+ "startTime": "",
+ "endTime": "",
+ "gpsLocation": "",
+ "file": ""
+ },
+ {
+ "_id": "696660bde56b708ddc8dfbfc",
+ "externalId": "Q77_4766403683020-1768317117025",
+ "question": [
+ "Quantity owned by household",
+ ""
+ ],
+ "tip": "",
+ "hint": "",
+ "responseType": "number",
+ "value": 1,
+ "isCompleted": false,
+ "showRemarks": false,
+ "remarks": "",
+ "visibleIf": "",
+ "options": [],
+ "sliderOptions": [],
+ "children": [],
+ "questionGroup": [
+ "A1"
+ ],
+ "questionType": "auto",
+ "modeOfCollection": "",
+ "usedForScoring": "",
+ "fileName": [],
+ "validation": {
+ "required": false,
+ "IsNumber": "true"
+ },
+ "accessibility": "",
+ "canBeNotApplicable": "false",
+ "instanceQuestions": [],
+ "isAGeneralQuestion": false,
+ "autoCapture": false,
+ "rubricLevel": "",
+ "sectionHeader": "",
+ "allowAudioRecording": false,
+ "page": "p4",
+ "questionNumber": "",
+ "prefillFromEntityProfile": false,
+ "entityFieldName": "",
+ "isEditable": true,
+ "showQuestionInPreview": false,
+ "createdFromQuestionId": "69666080e56b708ddc8dfb98",
+ "orgId": "brac_gbl",
+ "tenantId": "brac",
+ "reportType": "default",
+ "updatedAt": "2026-01-13T15:11:57.036Z",
+ "createdAt": "2026-01-13T15:10:56.856Z",
+ "deleted": false,
+ "__v": 0,
+ "evidenceMethod": "OB",
+ "payload": {
+ "criteriaId": "696660bde56b708ddc8dfc61",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "rubricLevel": ""
+ },
+ "startTime": "",
+ "endTime": 1768892283774,
+ "gpsLocation": "",
+ "file": ""
+ },
+ {
+ "_id": "696660bde56b708ddc8dfbfd",
+ "externalId": "Q78_4766403683020-1768317117026",
+ "question": [
+ "Quantity owned by you",
+ ""
+ ],
+ "tip": "",
+ "hint": "",
+ "responseType": "number",
+ "value": 1,
+ "isCompleted": false,
+ "showRemarks": false,
+ "remarks": "",
+ "visibleIf": "",
+ "options": [],
+ "sliderOptions": [],
+ "children": [],
+ "questionGroup": [
+ "A1"
+ ],
+ "questionType": "auto",
+ "modeOfCollection": "",
+ "usedForScoring": "",
+ "fileName": [],
+ "validation": {
+ "required": false,
+ "IsNumber": "true"
+ },
+ "accessibility": "",
+ "canBeNotApplicable": "false",
+ "instanceQuestions": [],
+ "isAGeneralQuestion": false,
+ "autoCapture": false,
+ "rubricLevel": "",
+ "sectionHeader": "",
+ "allowAudioRecording": false,
+ "page": "p4",
+ "questionNumber": "",
+ "prefillFromEntityProfile": false,
+ "entityFieldName": "",
+ "isEditable": true,
+ "showQuestionInPreview": false,
+ "createdFromQuestionId": "69666080e56b708ddc8dfb9f",
+ "orgId": "brac_gbl",
+ "tenantId": "brac",
+ "reportType": "default",
+ "updatedAt": "2026-01-13T15:11:57.036Z",
+ "createdAt": "2026-01-13T15:10:56.877Z",
+ "deleted": false,
+ "__v": 0,
+ "evidenceMethod": "OB",
+ "payload": {
+ "criteriaId": "696660bde56b708ddc8dfc61",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "rubricLevel": ""
+ },
+ "startTime": "",
+ "endTime": 1768892285684,
+ "gpsLocation": "",
+ "file": ""
+ },
+ {
+ "_id": "696660bde56b708ddc8dfbfe",
+ "externalId": "Q79_4766403683020-1768317117026",
+ "question": [
+ "Value per unit (R)",
+ ""
+ ],
+ "tip": "",
+ "hint": "",
+ "responseType": "number",
+ "value": 1,
+ "isCompleted": false,
+ "showRemarks": false,
+ "remarks": "",
+ "visibleIf": "",
+ "options": [],
+ "sliderOptions": [],
+ "children": [],
+ "questionGroup": [
+ "A1"
+ ],
+ "questionType": "auto",
+ "modeOfCollection": "",
+ "usedForScoring": "",
+ "fileName": [],
+ "validation": {
+ "required": false,
+ "IsNumber": "true"
+ },
+ "accessibility": "",
+ "canBeNotApplicable": "false",
+ "instanceQuestions": [],
+ "isAGeneralQuestion": false,
+ "autoCapture": false,
+ "rubricLevel": "",
+ "sectionHeader": "",
+ "allowAudioRecording": false,
+ "page": "p4",
+ "questionNumber": "",
+ "prefillFromEntityProfile": false,
+ "entityFieldName": "",
+ "isEditable": true,
+ "showQuestionInPreview": false,
+ "createdFromQuestionId": "69666080e56b708ddc8dfba6",
+ "orgId": "brac_gbl",
+ "tenantId": "brac",
+ "reportType": "default",
+ "updatedAt": "2026-01-13T15:11:57.036Z",
+ "createdAt": "2026-01-13T15:10:56.909Z",
+ "deleted": false,
+ "__v": 0,
+ "evidenceMethod": "OB",
+ "payload": {
+ "criteriaId": "696660bde56b708ddc8dfc61",
+ "responseType": "number",
+ "evidenceMethod": "OB",
+ "rubricLevel": ""
+ },
+ "startTime": "",
+ "endTime": 1768892288414,
+ "gpsLocation": "",
+ "file": ""
+ }
+ ]
+ ],
+ "responseType": "matrix",
+ "filesNotUploaded": []
+ },
+ "startTime": "",
+ "endTime": "",
+ "criteriaId": "696660bde56b708ddc8dfc61",
+ "responseType": "matrix",
+ "evidenceMethod": "OB",
+ "visibleIf": "",
+ "rubricLevel": "",
+ "countOfInstances": 1,
+ "options": [],
+ "externalId": "Q74_4766403683020-1768317117023",
+ "question": [
+ "Asset Inventory",
+ ""
+ ],
+ "questionNumber": "25",
+ "reportType": "default"
+ }
+ },
+ "completedDate": "2026-01-20T07:30:24.603Z",
+ "solutionInfo": {
+ "_id": "696660bde56b708ddc8dfc71",
+ "name": "Household Profile",
+ "description": "Household Profile",
+ "scoringSystem": null,
+ "questionSequenceByEcm": {
+ "OB": {
+ "S1": [
+ "Q1_4766403683020-1768917116969",
+ "Q2_4766403683020-1768317116969",
+ "Q3_4766403683020-1768317116970",
+ "Q4_4766403683020-1768317116971",
+ "Q5_4766403683020-1768317116971",
+ "Q6_4766403683020-1768317116972",
+ "Q7_4766403683020-1768317116973",
+ "Q8_4766403683020-1768317116974",
+ "Q9_4766403683020-1768317116974",
+ "Q10_4766403683020-1768317116975",
+ "Q11_4766403683020-1768317116976",
+ "Q12_4766403683020-1768317116977",
+ "Q13_4766403683020-1768317116977",
+ "Q14_4766403683020-1768317116978",
+ "Q15_4766403683020-1768317116979",
+ "Q16_4766403683020-1768317116981",
+ "Q17_4766403683020-1768317116981",
+ "Q18_4766403683020-1768317116982",
+ "Q19_4766403683020-1768317116983",
+ "Q20_4766403683020-1768317116983",
+ "Q21_4766403683020-1768317116984",
+ "Q22_4766403683020-1768317116985",
+ "Q23_4766403683020-1768317116985",
+ "Q24_4766403683020-1768317116986",
+ "Q25_4766403683020-1768317116987",
+ "Q26_4766403683020-1768317116988",
+ "Q27_4766403683020-1768317116988",
+ "Q28_4766403683020-1768317116989",
+ "Q29_4766403683020-1768317116990",
+ "Q30_4766403683020-1768317116991",
+ "Q31_4766403683020-1768317116991",
+ "Q32_4766403683020-1768317116992",
+ "Q33_4766403683020-1768317116994",
+ "Q34_4766403683020-1768317116995",
+ "Q35_4766403683020-1768317116995",
+ "Q36_4766403683020-1768317116996"
+ ],
+ "S2": [
+ "Q37_4766403683020-1768317116996",
+ "Q38_4766403683020-1768317116997",
+ "Q39_4766403683020-1768317116998",
+ "Q40_4766403683020-1768317116999",
+ "Q41_4766403683020-1768317116999",
+ "Q42_4766403683020-1768317117000",
+ "Q43_4766403683020-1768317117000",
+ "Q44_4766403683020-1768317117002",
+ "Q46_4766403683020-1768317117003",
+ "Q47_4766403683020-1768317117004",
+ "Q48_4766403683020-1768317117004",
+ "Q49_4766403683020-1768317117005",
+ "Q50_4766403683020-1768317117006",
+ "Q51_4766403683020-1768317117006",
+ "Q52_4766403683020-1768317117008",
+ "Q53_4766403683020-1768317117009",
+ "Q54_4766403683020-1768317117010",
+ "Q55_4766403683020-1768317117010",
+ "Q56_4766403683020-1768317117011",
+ "Q57_4766403683020-1768317117012",
+ "Q58_4766403683020-1768317117012",
+ "Q59_4766403683020-1768317117013",
+ "Q60_4766403683020-1768317117013",
+ "Q61_4766403683020-1768317117014",
+ "Q62_4766403683020-1768317117015",
+ "Q63_4766403683020-1768317117015",
+ "Q64_4766403683020-1768317117016",
+ "Q65_4766403683020-1768317117016",
+ "Q66_4766403683020-1768317117017",
+ "Q67_4766403683020-1768317117018",
+ "Q68_4766403683020-1768317117018",
+ "Q69_4766403683020-1768317117019",
+ "Q70_4766403683020-1768317117020",
+ "Q71_4766403683020-1768317117020",
+ "Q72_4766403683020-1768317117021",
+ "Q73_4766403683020-1768317117022",
+ "Q74_4766403683020-1768317117023",
+ "Q75_4766403683020-1768317117023",
+ "Q76_4766403683020-1768317117024",
+ "Q77_4766403683020-1768317117025",
+ "Q78_4766403683020-1768317117026",
+ "Q79_4766403683020-1768317117026"
+ ]
+ }
+ }
+ },
+ "entityTypeId": "6953d07ee83c1c0014713bf8"
+}
\ No newline at end of file
diff --git a/stream-jobs/user-mapping-stream-processor/src/main/resources/field-mappings.conf b/stream-jobs/user-mapping-stream-processor/src/main/resources/field-mappings.conf
index 81d78158..3f2cc44e 100644
--- a/stream-jobs/user-mapping-stream-processor/src/main/resources/field-mappings.conf
+++ b/stream-jobs/user-mapping-stream-processor/src/main/resources/field-mappings.conf
@@ -1,3 +1,8 @@
name = profile.name
+username = profile.username
+dob = profile.dob
+phone_code = profile.phoneCode
about = profile.about
-dob = profile.dob
\ No newline at end of file
+preferred_language = profile.preferredLanguage
+tenant_code = profile.tenantCode
+meta = profile.meta
\ No newline at end of file
diff --git a/stream-jobs/user-mapping-stream-processor/src/main/resources/user-mapping-stream.conf b/stream-jobs/user-mapping-stream-processor/src/main/resources/user-mapping-stream.conf
index e50bc344..9868fe7f 100644
--- a/stream-jobs/user-mapping-stream-processor/src/main/resources/user-mapping-stream.conf
+++ b/stream-jobs/user-mapping-stream-processor/src/main/resources/user-mapping-stream.conf
@@ -1,10 +1,10 @@
include "base-config.conf"
kafka {
- input.topic = "dev.userCreate"
- groupId = "dev.users"
- output.topic = "sl-metabase-user-dashboard-dev"
- output.mentoring.topic = "sl-metabase-mentoring-dashboard-dev"
+ input.topic = "qa.observation-submission"
+ groupId = "qa.users"
+ output.topic = "sl-metabase-user-dashboard-qa"
+ output.mentoring.topic = "sl-metabase-mentoring-dashboard-qa"
}
task {
diff --git a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/domain/ObservationEvent.scala b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/domain/ObservationEvent.scala
index a5120e0d..a56caa31 100644
--- a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/domain/ObservationEvent.scala
+++ b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/domain/ObservationEvent.scala
@@ -27,9 +27,10 @@ class ObservationEvent(eventMap: java.util.Map[String, Any], partition: Int, off
def eventType: String = readOrDefault[String]("eventType", null)
def studentId: String = {
- // Support both "id" (numeric) and "studentId" (string) for backward compatibility
+ // Support "id", "studentId", and "createdBy"
val idValue = readOrDefault[Any]("id", null)
val studentIdValue = readOrDefault[String]("studentId", null)
+ val createdByValue = readOrDefault[String]("createdBy", null)
if (idValue != null) {
// Convert numeric ID to string
@@ -40,6 +41,8 @@ class ObservationEvent(eventMap: java.util.Map[String, Any], partition: Int, off
}
} else if (studentIdValue != null) {
studentIdValue
+ } else if (createdByValue != null) {
+ createdByValue
} else {
null
}
@@ -56,15 +59,15 @@ class ObservationEvent(eventMap: java.util.Map[String, Any], partition: Int, off
}
def observationData: util.Map[String, Any] = {
- val data = readOrDefault[Any]("observationData", null)
- if (data == null) {
- new util.HashMap[String, Any]()
- } else {
- data match {
- case javaMap: util.Map[String, Any] => javaMap
- case scalaMap: scala.collection.Map[String, Any] => scalaMap.asJava
+ val userProfile = readOrDefault[Any]("userProfile", null)
+ if (userProfile != null) {
+ userProfile match {
+ case javaMap: util.Map[_, _] => javaMap.asInstanceOf[util.Map[String, Any]]
+ case scalaMap: scala.collection.Map[_, _] => scalaMap.asInstanceOf[scala.collection.Map[String, Any]].asJava
case _ => new util.HashMap[String, Any]()
}
+ } else {
+ new util.HashMap[String, Any]()
}
}
diff --git a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/functions/UserMappingStreamFunction.scala b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/functions/UserMappingStreamFunction.scala
index e5a308d0..c8381bfe 100644
--- a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/functions/UserMappingStreamFunction.scala
+++ b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/functions/UserMappingStreamFunction.scala
@@ -88,6 +88,11 @@ class UserMappingStreamFunction(config: UserMappingStreamConfig)(implicit val ma
logger.info(s"[UserMappingStreamFunction] Successfully updated profile for studentId=$studentId")
metrics.incCounter(config.successCount)
+ case Success(false) =>
+ println(s"[UserMappingStreamFunction] FAILED: API returned success=false for studentId=$studentId")
+ logger.warn(s"[UserMappingStreamFunction] API returned success=false for studentId=$studentId")
+ metrics.incCounter(config.skipCount)
+
case Failure(exception) =>
println(s"[UserMappingStreamFunction] FAILED: Could not update profile for studentId=$studentId: ${exception.getMessage}")
logger.error(s"[UserMappingStreamFunction] Failed to update profile for studentId=$studentId", exception)
diff --git a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/util/FieldMapper.scala b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/util/FieldMapper.scala
index 504cd466..fcbd42cd 100644
--- a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/util/FieldMapper.scala
+++ b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/util/FieldMapper.scala
@@ -43,8 +43,9 @@ object FieldMapper {
}
val configMap = config.entrySet().asScala.map { entry =>
- val key = entry.getKey
- val value = config.getString(key)
+ val rawKey = entry.getKey
+ val key = rawKey.replaceAll("\"", "")
+ val value = config.getString(rawKey)
(key, value)
}.toMap
@@ -59,6 +60,25 @@ object FieldMapper {
}
}
+ /**
+ * Check if a value is null or empty
+ * @param value The value to check
+ * @return true if value is null, empty string, or empty collection
+ */
+ private def isValueEmpty(value: Any): Boolean = {
+ if (value == null) {
+ return true
+ }
+
+ value match {
+ case s: String => s.trim.isEmpty
+ case coll: java.util.Collection[_] => coll.isEmpty
+ case arr: Array[_] => arr.isEmpty
+ case map: java.util.Map[_, _] => map.isEmpty
+ case _ => false // Other types (numbers, booleans) are not considered empty
+ }
+ }
+
/**
* Transform observation data to user profile patch format
*
@@ -94,7 +114,10 @@ object FieldMapper {
// Get value from observation data
val value = observationData.get(sourceField)
- if (value != null) {
+ // Check if value is null or empty, and skip if so
+ if (isValueEmpty(value)) {
+ println(s"[FieldMapper] Source field '$sourceField' is null or empty, skipping")
+ } else {
// Parse target path (e.g., "profile.phone" -> ["profile", "phone"])
val pathParts = targetPath.split("\\.")
@@ -125,8 +148,6 @@ object FieldMapper {
} else {
println(s"[FieldMapper] WARNING: Invalid target path format: $targetPath (expected format: 'profile.field')")
}
- } else {
- println(s"[FieldMapper] Source field '$sourceField' not found in observation data, skipping")
}
} catch {
case e: Exception =>
diff --git a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/util/UserApiClient.scala b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/util/UserApiClient.scala
index 5a858116..a0d41424 100644
--- a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/util/UserApiClient.scala
+++ b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/util/UserApiClient.scala
@@ -19,7 +19,7 @@ object UserApiClient {
// User Service Configuration
private val BASE_URL = "http://localhost:7001"
- private val AUTH_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImlkIjozMDg4LCJuYW1lIjoiQ2Fyb2wgTWlyYW5kYSIsInNlc3Npb25faWQiOjIzMDE4LCJvcmdhbml6YXRpb25faWRzIjpbIjY3Il0sIm9yZ2FuaXphdGlvbl9jb2RlcyI6WyJicmFjX2dibCJdLCJ0ZW5hbnRfY29kZSI6ImJyYWMiLCJvcmdhbml6YXRpb25zIjpbeyJpZCI6NjcsIm5hbWUiOiJCUkFDIEdCTCBvcmciLCJjb2RlIjoiYnJhY19nYmwiLCJkZXNjcmlwdGlvbiI6IkJSQUMgR0JMIG9yZyIsInN0YXR1cyI6IkFDVElWRSIsInJlbGF0ZWRfb3JncyI6bnVsbCwidGVuYW50X2NvZGUiOiJicmFjIiwibWV0YSI6bnVsbCwiY3JlYXRlZF9ieSI6bnVsbCwidXBkYXRlZF9ieSI6MSwicm9sZXMiOlt7ImlkIjoyMTMsInRpdGxlIjoic2Vzc2lvbl9tYW5hZ2VyIiwibGFiZWwiOiJMaW5rYWdlIENoYW1waW9uIiwidXNlcl90eXBlIjowLCJzdGF0dXMiOiJBQ1RJVkUiLCJvcmdhbml6YXRpb25faWQiOjY3LCJ2aXNpYmlsaXR5IjoiUFVCTElDIiwidGVuYW50X2NvZGUiOiJicmFjIiwidHJhbnNsYXRpb25zIjpudWxsfV19XX0sImlhdCI6MTc2ODM4Mzc1MCwiZXhwIjoxNzY4NDcwMTUwfQ.T8Ycr0X3bbVOCi63p8CdHlt9hAwClrKS9euGwV6ht78"
+ private val AUTH_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImlkIjozMDg3LCJuYW1lIjoiRmFyYWJpIEFobWVkdWxsYWgiLCJzZXNzaW9uX2lkIjoyMzAyMywib3JnYW5pemF0aW9uX2lkcyI6WyI2NyJdLCJvcmdhbml6YXRpb25fY29kZXMiOlsiYnJhY19nYmwiXSwidGVuYW50X2NvZGUiOiJicmFjIiwib3JnYW5pemF0aW9ucyI6W3siaWQiOjY3LCJuYW1lIjoiQlJBQyBHQkwgb3JnIiwiY29kZSI6ImJyYWNfZ2JsIiwiZGVzY3JpcHRpb24iOiJCUkFDIEdCTCBvcmciLCJzdGF0dXMiOiJBQ1RJVkUiLCJyZWxhdGVkX29yZ3MiOm51bGwsInRlbmFudF9jb2RlIjoiYnJhYyIsIm1ldGEiOm51bGwsImNyZWF0ZWRfYnkiOm51bGwsInVwZGF0ZWRfYnkiOjEsInJvbGVzIjpbeyJpZCI6MjEzLCJ0aXRsZSI6InNlc3Npb25fbWFuYWdlciIsImxhYmVsIjoiTGlua2FnZSBDaGFtcGlvbiIsInVzZXJfdHlwZSI6MCwic3RhdHVzIjoiQUNUSVZFIiwib3JnYW5pemF0aW9uX2lkIjo2NywidmlzaWJpbGl0eSI6IlBVQkxJQyIsInRlbmFudF9jb2RlIjoiYnJhYyIsInRyYW5zbGF0aW9ucyI6bnVsbH1dfV19LCJpYXQiOjE3NjkwNjA4NTMsImV4cCI6MTc2OTE0NzI1M30.CPTelpSKG7wHA7WccMMwVI7rk0QajCt_Baf1rh0vCcw"
println(s"[UserApiClient] Initialized with BASE_URL: $BASE_URL")
println(s"[UserApiClient] AUTH_TOKEN configured: ${if (AUTH_TOKEN.nonEmpty) "***" else "NOT SET"}")
diff --git a/stream-jobs/user-mapping-stream-processor/src/test/scala/org/shikshalokam/user/mapping/stream/processor/spec/ObservationEventSource.scala b/stream-jobs/user-mapping-stream-processor/src/test/scala/org/shikshalokam/user/mapping/stream/processor/spec/ObservationEventSource.scala
index 84daaa2b..d7e24808 100644
--- a/stream-jobs/user-mapping-stream-processor/src/test/scala/org/shikshalokam/user/mapping/stream/processor/spec/ObservationEventSource.scala
+++ b/stream-jobs/user-mapping-stream-processor/src/test/scala/org/shikshalokam/user/mapping/stream/processor/spec/ObservationEventSource.scala
@@ -9,7 +9,11 @@ import org.shikshalokam.user.mapping.stream.processor.fixture.ObservationEventsM
class ObservationEventSource extends SourceFunction[ObservationEvent] {
override def run(ctx: SourceContext[ObservationEvent]): Unit = {
- ctx.collect(new ObservationEvent(JSONUtil.deserialize[java.util.Map[String, Any]](ObservationEventsMock.OBSERVATION_SUBMITTED), 0, 0))
+ val filePath = "/home/ttpl-rt-221/elevate/data-pipeline/stream-jobs/user-mapping-stream-processor/obs_kafka_response.json"
+ val source = scala.io.Source.fromFile(filePath)
+ val jsonContent = try source.mkString finally source.close()
+
+ ctx.collect(new ObservationEvent(JSONUtil.deserialize[java.util.Map[String, Any]](jsonContent), 0, 0))
}
override def cancel(): Unit = {}
From 63522724d5cf2eabf6cda061f605960b1271a51b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E2=80=9CKomal?= <“komal_m@tekditechnologies.com”>
Date: Fri, 30 Jan 2026 17:23:45 +0530
Subject: [PATCH 03/22] Task #252259 feat: When the household profile
Observation is submitted, participant's profile should be updated
---
jobs-core/src/main/resources/base-config.conf | 2 +-
.../org/shikshalokam/job/BaseJobConfig.scala | 14 +-
.../src/main/resources/field-mappings.conf | 2 +-
.../main/resources/user-mapping-stream.conf | 13 +-
.../stream/processor/domain/Event.scala | 26 +++-
.../processor/domain/ObservationEvent.scala | 58 +++++---
.../functions/UserMappingStreamFunction.scala | 137 ++++++++++++++----
.../task/UserMappingStreamTask.scala | 52 ++++++-
.../stream/processor/util/FieldMapper.scala | 131 ++++++++++++++---
.../stream/processor/util/UserApiClient.scala | 39 ++---
10 files changed, 375 insertions(+), 99 deletions(-)
diff --git a/jobs-core/src/main/resources/base-config.conf b/jobs-core/src/main/resources/base-config.conf
index 0ac79975..55c2f7e0 100644
--- a/jobs-core/src/main/resources/base-config.conf
+++ b/jobs-core/src/main/resources/base-config.conf
@@ -1,5 +1,5 @@
kafka {
- broker-servers = "user-kafka-1:9092"
+ broker-servers = "kafka:29092"
zookeeper = "zookeeper:2181"
}
diff --git a/jobs-core/src/main/scala/org/shikshalokam/job/BaseJobConfig.scala b/jobs-core/src/main/scala/org/shikshalokam/job/BaseJobConfig.scala
index 2d1e9af1..cf612ab3 100644
--- a/jobs-core/src/main/scala/org/shikshalokam/job/BaseJobConfig.scala
+++ b/jobs-core/src/main/scala/org/shikshalokam/job/BaseJobConfig.scala
@@ -35,9 +35,16 @@ class BaseJobConfig(val config: Config, val jobName: String) extends Serializabl
def kafkaConsumerProperties: Properties = {
val properties = new Properties()
- properties.setProperty("bootstrap.servers", kafkaBrokerServers)
+ properties.setProperty("bootstrap.servers", "kafka:29092")
properties.setProperty("group.id", groupId)
properties.setProperty(ConsumerConfig.ISOLATION_LEVEL_CONFIG, "read_committed")
+
+ // Add timeout configurations to handle connection issues better
+ // Default timeout is 30 seconds, increase to 60 seconds for better reliability
+ properties.setProperty(ConsumerConfig.REQUEST_TIMEOUT_MS_CONFIG, "60000")
+ properties.setProperty(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, "30000")
+ properties.setProperty(ConsumerConfig.METADATA_MAX_AGE_CONFIG, "300000")
+
kafkaAutoOffsetReset.map {
properties.setProperty("auto.offset.reset", _)
}
@@ -50,6 +57,11 @@ class BaseJobConfig(val config: Config, val jobName: String) extends Serializabl
properties.put(ProducerConfig.LINGER_MS_CONFIG, new Integer(10))
properties.put(ProducerConfig.BATCH_SIZE_CONFIG, new Integer(16384 * 4))
properties.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, "snappy")
+
+ // Add timeout configurations for producer
+ properties.setProperty(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG, "60000")
+ properties.setProperty(ProducerConfig.DELIVERY_TIMEOUT_MS_CONFIG, "120000")
+
properties
}
diff --git a/stream-jobs/user-mapping-stream-processor/src/main/resources/field-mappings.conf b/stream-jobs/user-mapping-stream-processor/src/main/resources/field-mappings.conf
index 3f2cc44e..c781df81 100644
--- a/stream-jobs/user-mapping-stream-processor/src/main/resources/field-mappings.conf
+++ b/stream-jobs/user-mapping-stream-processor/src/main/resources/field-mappings.conf
@@ -5,4 +5,4 @@ phone_code = profile.phoneCode
about = profile.about
preferred_language = profile.preferredLanguage
tenant_code = profile.tenantCode
-meta = profile.meta
\ No newline at end of file
+# meta = profile.meta
\ No newline at end of file
diff --git a/stream-jobs/user-mapping-stream-processor/src/main/resources/user-mapping-stream.conf b/stream-jobs/user-mapping-stream-processor/src/main/resources/user-mapping-stream.conf
index 9868fe7f..c141b59a 100644
--- a/stream-jobs/user-mapping-stream-processor/src/main/resources/user-mapping-stream.conf
+++ b/stream-jobs/user-mapping-stream-processor/src/main/resources/user-mapping-stream.conf
@@ -1,10 +1,15 @@
include "base-config.conf"
kafka {
- input.topic = "qa.observation-submission"
- groupId = "qa.users"
- output.topic = "sl-metabase-user-dashboard-qa"
- output.mentoring.topic = "sl-metabase-mentoring-dashboard-qa"
+ # broker-servers is inherited from base-config.conf ("kafka:9092" for Docker)
+ # Only override if running outside Docker or using a different Kafka instance:
+ # broker-servers = "localhost:9092" # For local development outside Docker
+ # broker-servers = "your-kafka-host:9092" # For remote Kafka instance
+
+ input.topic = "dev.observation-submission"
+ groupId = "dev.users"
+ output.topic = "sl-metabase-user-dashboard-dev"
+ output.mentoring.topic = "sl-metabase-mentoring-dashboard-dev"
}
task {
diff --git a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/domain/Event.scala b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/domain/Event.scala
index e95fdce7..dbc029e5 100644
--- a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/domain/Event.scala
+++ b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/domain/Event.scala
@@ -5,23 +5,24 @@ import org.shikshalokam.job.domain.reader.JobRequest
import java.sql.Timestamp
import java.text.SimpleDateFormat
import java.time.Instant
+import scala.collection.JavaConverters._
import scala.language.postfixOps
class Event(eventMap: java.util.Map[String, Any], partition: Int, offset: Long) extends JobRequest(eventMap, partition, offset) {
def eventType: String = readOrDefault[String]("eventType", null)
- def userId: Int = readOrDefault[Int]("entityId", -1)
+ def userId: Int = readOrDefault[Int]("id", -1)
def tenantCode: String = extractValue[String]("tenant_code").orNull
- def username: String = extractValue[String]("username").orNull
+// def username: String = extractValue[String]("username").orNull
def name: String = extractValue[String]("name").orNull
def status: String = extractValue[String]("status").orNull
- def isDeleted: Boolean = extractValue[Boolean]("deleted").getOrElse(false)
+// def isDeleted: Boolean = extractValue[Boolean]("deleted").getOrElse(false)
def createdBy: Int = extractValue[Int]("created_by").getOrElse(-1)
@@ -128,5 +129,24 @@ class Event(eventMap: java.util.Map[String, Any], partition: Int, offset: Long)
case _ => new Timestamp(System.currentTimeMillis())
}
+ /**
+ * Extract userProfile from observation event
+ * Used for observation-submission events from Kafka topic dev.observation-submission
+ *
+ * @return Map containing userProfile data, or empty map if not found
+ */
+ def userProfile: java.util.Map[String, Any] = {
+ val userProfileValue = readOrDefault[Any]("userProfile", null)
+ if (userProfileValue != null) {
+ userProfileValue match {
+ case javaMap: java.util.Map[_, _] => javaMap.asInstanceOf[java.util.Map[String, Any]]
+ case scalaMap: scala.collection.Map[_, _] => scalaMap.asInstanceOf[scala.collection.Map[String, Any]].asJava
+ case _ => new java.util.HashMap[String, Any]()
+ }
+ } else {
+ new java.util.HashMap[String, Any]()
+ }
+ }
+
}
diff --git a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/domain/ObservationEvent.scala b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/domain/ObservationEvent.scala
index a56caa31..d0cfe1d0 100644
--- a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/domain/ObservationEvent.scala
+++ b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/domain/ObservationEvent.scala
@@ -26,25 +26,49 @@ class ObservationEvent(eventMap: java.util.Map[String, Any], partition: Int, off
def eventType: String = readOrDefault[String]("eventType", null)
- def studentId: String = {
- // Support "id", "studentId", and "createdBy"
- val idValue = readOrDefault[Any]("id", null)
- val studentIdValue = readOrDefault[String]("studentId", null)
- val createdByValue = readOrDefault[String]("createdBy", null)
+ def id: String = {
+ // Priority order: userProfile.id > entityId > id (root) > oldValues.id/newValues.id
+
+ // 1. Try userProfile.id first (highest priority)
+ val userProfileId = readOrDefault[Any]("userProfile.id", null)
+ if (userProfileId != null) {
+ return convertToString(userProfileId)
+ }
+
+ // 2. Try entityId at root level
+ val entityIdValue = readOrDefault[Any]("entityId", null)
+ if (entityIdValue != null) {
+ return convertToString(entityIdValue)
+ }
+ // 3. Try id at root level
+ val idValue = readOrDefault[Any]("id", null)
if (idValue != null) {
- // Convert numeric ID to string
- idValue match {
- case n: Number => n.toString
- case s: String => s
- case _ => idValue.toString
+ return convertToString(idValue)
+ }
+
+ // 4. For update events, try oldValues.id or newValues.id
+ if (eventType == "update" || eventType == "bulk-update") {
+ val newValuesId = readOrDefault[Any]("newValues.id", null)
+ if (newValuesId != null) {
+ return convertToString(newValuesId)
}
- } else if (studentIdValue != null) {
- studentIdValue
- } else if (createdByValue != null) {
- createdByValue
- } else {
- null
+
+ val oldValuesId = readOrDefault[Any]("oldValues.id", null)
+ if (oldValuesId != null) {
+ return convertToString(oldValuesId)
+ }
+ }
+
+ null
+ }
+
+ private def convertToString(value: Any): String = {
+ if (value == null) return null
+ value match {
+ case n: Number => n.toString
+ case s: String => s
+ case _ => value.toString
}
}
@@ -72,6 +96,6 @@ class ObservationEvent(eventMap: java.util.Map[String, Any], partition: Int, off
}
override def toString: String = {
- s"ObservationEvent(eventType=$eventType, studentId=$studentId, organizationId=$organizationId)"
+ s"ObservationEvent(eventType=$eventType, id=$id, organizationId=$organizationId)"
}
}
diff --git a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/functions/UserMappingStreamFunction.scala b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/functions/UserMappingStreamFunction.scala
index c8381bfe..8dec4698 100644
--- a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/functions/UserMappingStreamFunction.scala
+++ b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/functions/UserMappingStreamFunction.scala
@@ -8,6 +8,7 @@ import org.shikshalokam.job.user.mapping.stream.processor.task.UserMappingStream
import org.shikshalokam.job.user.mapping.stream.processor.util.{FieldMapper, UserApiClient}
import org.shikshalokam.job.{BaseProcessFunction, Metrics}
import org.slf4j.LoggerFactory
+import ujson.Obj
import java.util
import scala.collection.JavaConverters._
@@ -36,72 +37,143 @@ class UserMappingStreamFunction(config: UserMappingStreamConfig)(implicit val ma
println("[UserMappingStreamFunction] Closing...")
}
+ /**
+ * Recursively print all fields and values from a map structure
+ */
+ private def printAllFields(map: java.util.Map[String, Any], prefix: String = "", depth: Int = 0, maxDepth: Int = 5): Unit = {
+ if (depth > maxDepth) {
+ val fullKey = if (prefix.isEmpty) "[Max depth reached]" else s"$prefix[Max depth reached, truncating...]"
+ println(s"[UserMappingStreamFunction] $fullKey")
+ return
+ }
+
+ if (map == null || map.isEmpty) {
+ val fullKey = if (prefix.isEmpty) "[Empty map]" else s"$prefix = [Empty map]"
+ println(s"[UserMappingStreamFunction] $fullKey")
+ return
+ }
+
+ map.asScala.foreach { case (key, value) =>
+ val fullKey = if (prefix.isEmpty) key else s"$prefix.$key"
+ value match {
+ case nestedMap: java.util.Map[_, _] =>
+ println(s"[UserMappingStreamFunction] $fullKey = [Map with ${nestedMap.size()} entries]")
+ printAllFields(nestedMap.asInstanceOf[java.util.Map[String, Any]], fullKey, depth + 1, maxDepth)
+ case list: java.util.List[_] =>
+ println(s"[UserMappingStreamFunction] $fullKey = [List with ${list.size()} items]")
+ list.asScala.zipWithIndex.foreach { case (item, idx) =>
+ item match {
+ case itemMap: java.util.Map[_, _] =>
+ println(s"[UserMappingStreamFunction] $fullKey[$idx] = [Map]")
+ printAllFields(itemMap.asInstanceOf[java.util.Map[String, Any]], s"$fullKey[$idx]", depth + 1, maxDepth)
+ case _ =>
+ println(s"[UserMappingStreamFunction] $fullKey[$idx] = $item")
+ }
+ }
+ case _ =>
+ val valueStr = if (value != null) value.toString else "null"
+ val truncatedValue = if (valueStr.length > 200) valueStr.substring(0, 200) + "..." else valueStr
+ println(s"[UserMappingStreamFunction] $fullKey = $truncatedValue")
+ }
+ }
+ }
+
override def processElement(event: ObservationEvent, context: ProcessFunction[ObservationEvent, ObservationEvent]#Context, metrics: Metrics): Unit = {
try {
println(s"***************** Start Processing Observation Event *****************")
+
+ // Print all fields from the event map
+ println(s"[UserMappingStreamFunction] ========== ALL EVENT FIELDS ==========")
+ val eventMap = event.getMap()
+ println(s"[UserMappingStreamFunction] Total top-level fields: ${eventMap.size()}")
+ printAllFields(eventMap)
+ println(s"[UserMappingStreamFunction] ========================================")
+
+ // Print JSON representation
+ println(s"[UserMappingStreamFunction] Full Event JSON:")
+ println(s"[UserMappingStreamFunction] ${event.getJson()}")
+
println(s"[UserMappingStreamFunction] Event Type: ${event.eventType}")
- println(s"[UserMappingStreamFunction] Student ID: ${event.studentId}")
+ println(s"[UserMappingStreamFunction] ID: ${event.id}")
println(s"[UserMappingStreamFunction] Organization ID: ${event.organizationId}")
// Update total events count metric
metrics.incCounter(config.totalEventsCount)
- // Validate event
- val studentId = event.studentId
- if (studentId == null || studentId.trim.isEmpty) {
- println(s"[UserMappingStreamFunction] ERROR: studentId/id is null or empty, skipping event")
- logger.error("[UserMappingStreamFunction] studentId/id is null or empty")
+ // Validate event - ensure id is present
+ val id = event.id
+ if (id == null || id.trim.isEmpty) {
+ println(s"[UserMappingStreamFunction] ERROR: id is null or empty, skipping event")
+ logger.error("[UserMappingStreamFunction] id is null or empty")
metrics.incCounter(config.skipCount)
return
}
- if (event.observationData == null || event.observationData.isEmpty) {
- println(s"[UserMappingStreamFunction] WARNING: observationData is null or empty for studentId=$studentId")
- logger.warn(s"[UserMappingStreamFunction] observationData is null or empty for studentId=$studentId")
+ // Extract userProfile from observation event
+ // The observationData method already extracts userProfile from the event
+ val userProfile = event.observationData
+
+ if (userProfile == null || userProfile.isEmpty) {
+ println(s"[UserMappingStreamFunction] WARNING: userProfile is null or empty for id=$id, skipping event")
+ logger.warn(s"[UserMappingStreamFunction] userProfile is null or empty for id=$id")
metrics.incCounter(config.skipCount)
return
}
- // Extract observation data
- val observationData = event.observationData
- println(s"[UserMappingStreamFunction] Observation data keys: ${observationData.keySet().asScala.mkString(", ")}")
+ println(s"[UserMappingStreamFunction] userProfile keys: ${userProfile.keySet().asScala.mkString(", ")}")
- // Transform observation data to profile format using FieldMapper
- println(s"[UserMappingStreamFunction] Transforming observation data to profile format...")
- val profileData = FieldMapper.transform(observationData)
+ // Transform userProfile data to profile format using FieldMapper
+ // FieldMapper will:
+ // 1. Only map allowed fields (name, username, dob, phoneCode, about, preferredLanguage, tenantCode, meta)
+ // 2. Skip fields that are null, empty, or missing
+ // 3. Return a profile object with only non-empty fields
+ println(s"[UserMappingStreamFunction] Transforming userProfile to profile format...")
+ val profileData = FieldMapper.transform(userProfile)
- // Check if profile data is empty
+ // Check if profile data contains any valid (non-empty) fields
+ // Only call API if at least one field is eligible for update
val profileObj = profileData.value.get("profile")
- if (profileObj.isEmpty || profileObj.get.asInstanceOf[ujson.Obj].value.isEmpty) {
- println(s"[UserMappingStreamFunction] WARNING: No profile data to update after transformation for studentId=$studentId")
- logger.warn(s"[UserMappingStreamFunction] No profile data to update after transformation for studentId=$studentId")
+ if (profileObj.isEmpty) {
+ println(s"[UserMappingStreamFunction] WARNING: No profile object in transformed data for id=$id, skipping API call")
+ logger.warn(s"[UserMappingStreamFunction] No profile object in transformed data for id=$id")
+ metrics.incCounter(config.skipCount)
+ return
+ }
+
+ val profileFields = profileObj.get.asInstanceOf[ujson.Obj].value
+ if (profileFields.isEmpty) {
+ println(s"[UserMappingStreamFunction] INFO: No valid (non-empty) fields to update for id=$id. All fields were null, empty, or missing. Skipping API call.")
+ logger.info(s"[UserMappingStreamFunction] No valid fields to update for id=$id - all fields were empty/null/missing")
metrics.incCounter(config.skipCount)
return
}
+ println(s"[UserMappingStreamFunction] Found ${profileFields.size} valid field(s) to update: ${profileFields.keys.mkString(", ")}")
+
// Call User Service API to patch the profile
- println(s"[UserMappingStreamFunction] Calling UserApiClient.patchProfile for studentId=$studentId...")
- UserApiClient.patchProfile(studentId, profileData) match {
+ // Only non-empty fields will be included in the update request
+ println(s"[UserMappingStreamFunction] Calling UserApiClient.patchProfile for id=$id...")
+ UserApiClient.patchProfile(id, profileData) match {
case Success(true) =>
- println(s"[UserMappingStreamFunction] SUCCESS: Profile updated for studentId=$studentId")
- logger.info(s"[UserMappingStreamFunction] Successfully updated profile for studentId=$studentId")
+ println(s"[UserMappingStreamFunction] SUCCESS: Profile updated for id=$id with ${profileFields.size} field(s)")
+ logger.info(s"[UserMappingStreamFunction] Successfully updated profile for id=$id with fields: ${profileFields.keys.mkString(", ")}")
metrics.incCounter(config.successCount)
case Success(false) =>
- println(s"[UserMappingStreamFunction] FAILED: API returned success=false for studentId=$studentId")
- logger.warn(s"[UserMappingStreamFunction] API returned success=false for studentId=$studentId")
+ println(s"[UserMappingStreamFunction] FAILED: API returned success=false for id=$id")
+ logger.warn(s"[UserMappingStreamFunction] API returned success=false for id=$id")
metrics.incCounter(config.skipCount)
case Failure(exception) =>
- println(s"[UserMappingStreamFunction] FAILED: Could not update profile for studentId=$studentId: ${exception.getMessage}")
- logger.error(s"[UserMappingStreamFunction] Failed to update profile for studentId=$studentId", exception)
+ println(s"[UserMappingStreamFunction] FAILED: Could not update profile for id=$id: ${exception.getMessage}")
+ logger.error(s"[UserMappingStreamFunction] Failed to update profile for id=$id", exception)
metrics.incCounter(config.skipCount)
- // Re-throw to trigger Flink retry mechanism if configured
- throw new Exception(s"Failed to update profile for studentId=$studentId", exception)
+ // Log error but continue processing - don't crash the task for API failures
+ // The error is already logged and metrics are updated
}
- println(s"***************** Completed Processing Observation Event for studentId=$studentId *****************")
+ println(s"***************** Completed Processing Observation Event for id=$id *****************")
} catch {
case e: Exception =>
@@ -109,8 +181,9 @@ class UserMappingStreamFunction(config: UserMappingStreamConfig)(implicit val ma
logger.error(s"[UserMappingStreamFunction] Exception processing observation event", e)
e.printStackTrace()
metrics.incCounter(config.skipCount)
- // Re-throw to trigger Flink retry mechanism if configured
- throw e
+ // Log error but continue processing - don't crash the task
+ // This ensures the pipeline continues processing other events even if one fails
+ // For truly fatal errors, Flink's checkpointing and monitoring will detect issues
}
}
diff --git a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/task/UserMappingStreamTask.scala b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/task/UserMappingStreamTask.scala
index c9b7af17..20560560 100644
--- a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/task/UserMappingStreamTask.scala
+++ b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/task/UserMappingStreamTask.scala
@@ -48,11 +48,55 @@ class UserMappingStreamTask(config: UserMappingStreamConfig, kafkaConnector: Fli
object UserMappingStreamTask {
def main(args: Array[String]): Unit = {
println("Starting up the User Mapping Stream Job")
- val configFilePath = Option(ParameterTool.fromArgs(args).get("config.file.path"))
- val config = configFilePath.map {
- path => ConfigFactory.parseFile(new File(path)).resolve()
- }.getOrElse(ConfigFactory.load("user-mapping-stream.conf").withFallback(ConfigFactory.systemEnvironment()))
+ val parameterTool = ParameterTool.fromArgs(args)
+ val configFilePath = Option(parameterTool.get("config.file.path"))
+
+ // Load config with proper precedence: system environment > config file > base config
+ val baseConfig = configFilePath.map {
+ path => ConfigFactory.parseFile(new File(path))
+ }.getOrElse(ConfigFactory.load("user-mapping-stream.conf"))
+
+ // System environment variables take precedence, then config file, then base config
+ val config = ConfigFactory.systemEnvironment()
+ .withFallback(baseConfig)
+ .resolve()
+
val userMappingStreamConfig = new UserMappingStreamConfig(config)
+
+ // Log Kafka configuration for debugging
+ val brokerServers = userMappingStreamConfig.kafkaBrokerServers
+ println(s"[UserMappingStreamTask] ========================================")
+ println(s"[UserMappingStreamTask] Kafka Configuration:")
+ println(s"[UserMappingStreamTask] Broker Servers: $brokerServers")
+ println(s"[UserMappingStreamTask] Group ID: ${userMappingStreamConfig.groupId}")
+ println(s"[UserMappingStreamTask] Input Topic: ${userMappingStreamConfig.inputTopic}")
+ println(s"[UserMappingStreamTask] Output Topic: ${userMappingStreamConfig.outputTopic}")
+ println(s"[UserMappingStreamTask] ========================================")
+
+ // Validate Kafka broker servers configuration
+ if (brokerServers == null || brokerServers.trim.isEmpty) {
+ throw new IllegalArgumentException(
+ "Kafka broker-servers is not configured. Please set kafka.broker-servers in your config file " +
+ "or set KAFKA_BROKER_SERVERS environment variable."
+ )
+ }
+
+ // Check if broker-servers is the default Docker value and warn if it might not resolve
+ if (brokerServers == "kafka:9092") {
+ println(s"[UserMappingStreamTask] WARNING: Using default Docker broker-servers 'kafka:9092'. " +
+ "If running outside Docker, this may not resolve. Set kafka.broker-servers in config or KAFKA_BROKER_SERVERS env var.")
+ }
+
+ // Provide troubleshooting information
+ println(s"[UserMappingStreamTask] Troubleshooting:")
+ println(s"[UserMappingStreamTask] If you see 'TimeoutException: Timeout expired while fetching topic metadata':")
+ println(s"[UserMappingStreamTask] 1. Verify Kafka is running: Check if Kafka broker is accessible at $brokerServers")
+ println(s"[UserMappingStreamTask] 2. Test connection: Try 'telnet ' or 'nc -zv '")
+ println(s"[UserMappingStreamTask] 3. Check firewall: Ensure port is not blocked")
+ println(s"[UserMappingStreamTask] 4. Verify address: Confirm the broker address is correct in your config")
+ println(s"[UserMappingStreamTask] 5. Check topics exist: Verify topics '${userMappingStreamConfig.inputTopic}' and '${userMappingStreamConfig.outputTopic}' exist")
+ println(s"[UserMappingStreamTask]")
+
val kafkaUtil = new FlinkKafkaConnector(userMappingStreamConfig)
val task = new UserMappingStreamTask(userMappingStreamConfig, kafkaUtil)
task.process()
diff --git a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/util/FieldMapper.scala b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/util/FieldMapper.scala
index fcbd42cd..680e9f58 100644
--- a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/util/FieldMapper.scala
+++ b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/util/FieldMapper.scala
@@ -2,7 +2,7 @@ package org.shikshalokam.job.user.mapping.stream.processor.util
import com.typesafe.config.ConfigFactory
import org.slf4j.LoggerFactory
-import ujson.{Js, Obj}
+import ujson.{Arr, Js, Null, Obj, read, write}
import java.util
import scala.collection.JavaConverters._
@@ -60,12 +60,63 @@ object FieldMapper {
}
}
+ /**
+ * Convert a Java Map to ujson.Obj recursively
+ */
+ private def javaMapToUjsonObj(map: java.util.Map[_, _]): ujson.Obj = {
+ val obj = Obj()
+ map.asScala.foreach { case (k, v) =>
+ val key = k.toString
+ obj.value(key) = anyToUjsonValue(v)
+ }
+ obj
+ }
+
+ /**
+ * Convert a Java Collection to ujson.Arr recursively
+ */
+ private def javaCollectionToUjsonArr(coll: java.util.Collection[_]): ujson.Arr = {
+ val arr = ujson.Arr()
+ coll.asScala.foreach { item =>
+ arr.value += anyToUjsonValue(item)
+ }
+ arr
+ }
+
+ /**
+ * Convert any value to ujson.Value recursively
+ */
+ private def anyToUjsonValue(value: Any): ujson.Value = {
+ value match {
+ case null => ujson.Null
+ case s: String => s
+ case n: Number => n.doubleValue()
+ case i: Int => i
+ case l: Long => l
+ case d: Double => d
+ case f: Float => f
+ case b: Boolean => b
+ case map: java.util.Map[_, _] => javaMapToUjsonObj(map)
+ case coll: java.util.Collection[_] => javaCollectionToUjsonArr(coll)
+ case arr: Array[_] =>
+ val ujsonArr = ujson.Arr()
+ arr.foreach { item => ujsonArr.value += anyToUjsonValue(item) }
+ ujsonArr
+ case _ => value.toString
+ }
+ }
+
/**
* Check if a value is null or empty
+ * Used to determine if a field should be skipped during mapping
+ *
+ * Rule: Update a field only if it has a valid value
+ * If a field is null, empty string, or missing → do not update it
+ *
* @param value The value to check
- * @return true if value is null, empty string, or empty collection
+ * @return true if value is null, empty string, or empty collection (should be skipped)
*/
- private def isValueEmpty(value: Any): Boolean = {
+ def isValueEmpty(value: Any): Boolean = {
if (value == null) {
return true
}
@@ -80,54 +131,79 @@ object FieldMapper {
}
/**
- * Transform observation data to user profile patch format
+ * Transform observation data (userProfile) to user profile patch format
+ *
+ * Only maps the following allowed fields from userProfile:
+ * - name
+ * - username
+ * - dob
+ * - phoneCode (from phone_code)
+ * - about
+ * - preferredLanguage (from preferred_language)
+ * - tenantCode (from tenant_code)
+ * - meta
+ *
+ * Important Rule: Update a field only if it has a valid value
+ * If a field is null, empty string, or missing → do not update it
+ * Existing user data should not be overwritten with empty values
*
- * Input observationData example:
+ * Input observationData (userProfile) example:
* {
* "name": "Carol Miranda Updated Two",
* "about": "admin Update",
- * "dob": "22-12-1990"
+ * "dob": "22-12-1990",
+ * "phone_code": "+91",
+ * "preferred_language": "en",
+ * "tenant_code": "qa",
+ * "meta": {"key": "value"}
* }
*
* Output profile format:
* {
* "profile": {
- * "name": "Carol Miranda Updated One",
+ * "name": "Carol Miranda Updated Two",
* "about": "admin Update",
- * "dob": "22-12-1990"
+ * "dob": "22-12-1990",
+ * "phoneCode": "+91",
+ * "preferredLanguage": "en",
+ * "tenantCode": "qa",
+ * "meta": {"key": "value"}
* }
* }
*
- * @param observationData Map containing observation field values
- * @return JsObject representing the profile patch structure
+ * @param observationData Map containing userProfile field values from observation event
+ * @return JsObject representing the profile patch structure with only non-empty fields
*/
def transform(observationData: util.Map[String, Any]): Js.Obj = {
try {
- println(s"[FieldMapper] Starting transformation of observation data")
- println(s"[FieldMapper] Input observationData keys: ${observationData.keySet().asScala.mkString(", ")}")
+ println(s"[FieldMapper] Starting transformation of userProfile data")
+ println(s"[FieldMapper] Input userProfile keys: ${observationData.keySet().asScala.mkString(", ")}")
val profileObj = Obj()
+ var mappedFieldsCount = 0
- // Iterate through each mapping
+ // Iterate through each configured mapping (only allowed fields are in the config)
mappings.foreach { case (sourceField, targetPath) =>
try {
- // Get value from observation data
+ // Get value from observation data (userProfile)
val value = observationData.get(sourceField)
// Check if value is null or empty, and skip if so
+ // This ensures we only update fields with valid values
if (isValueEmpty(value)) {
- println(s"[FieldMapper] Source field '$sourceField' is null or empty, skipping")
+ println(s"[FieldMapper] Source field '$sourceField' is null, empty, or missing - skipping (will not overwrite existing user data)")
} else {
- // Parse target path (e.g., "profile.phone" -> ["profile", "phone"])
+ // Parse target path (e.g., "profile.phoneCode" -> ["profile", "phoneCode"])
val pathParts = targetPath.split("\\.")
if (pathParts.length >= 2) {
val rootKey = pathParts(0) // e.g., "profile"
- val fieldKey = pathParts(1) // e.g., "phone"
+ val fieldKey = pathParts(1) // e.g., "phoneCode"
// We expect all mappings to be under "profile", so rootKey should be "profile"
if (rootKey == "profile") {
// Set the field value directly in profile object
+ // Only non-empty values reach this point
value match {
case s: String => profileObj.value(fieldKey) = s
case n: Number =>
@@ -138,9 +214,28 @@ object FieldMapper {
case d: Double => profileObj.value(fieldKey) = d
case f: Float => profileObj.value(fieldKey) = f
case b: Boolean => profileObj.value(fieldKey) = b
+ case map: java.util.Map[_, _] =>
+ // Handle nested objects like meta - convert directly to ujson.Obj
+ try {
+ profileObj.value(fieldKey) = javaMapToUjsonObj(map)
+ } catch {
+ case e: Exception =>
+ logger.warn(s"[FieldMapper] Could not convert map to JSON for field $fieldKey, using string representation", e)
+ profileObj.value(fieldKey) = map.toString
+ }
+ case coll: java.util.Collection[_] =>
+ // Handle collections - convert directly to ujson.Arr
+ try {
+ profileObj.value(fieldKey) = javaCollectionToUjsonArr(coll)
+ } catch {
+ case e: Exception =>
+ logger.warn(s"[FieldMapper] Could not convert collection to JSON for field $fieldKey, using string representation", e)
+ profileObj.value(fieldKey) = coll.toString
+ }
case _ => profileObj.value(fieldKey) = value.toString
}
+ mappedFieldsCount += 1
println(s"[FieldMapper] Mapped $sourceField -> $targetPath = $value")
} else {
println(s"[FieldMapper] WARNING: Root key '$rootKey' is not 'profile', skipping field $sourceField")
@@ -158,7 +253,7 @@ object FieldMapper {
// Return the result with "profile" as root
val result = Obj("profile" -> profileObj)
- println(s"[FieldMapper] Transformation complete. Result: ${result.render()}")
+ println(s"[FieldMapper] Transformation complete. Mapped $mappedFieldsCount non-empty fields. Result: ${result.render()}")
result
} catch {
diff --git a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/util/UserApiClient.scala b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/util/UserApiClient.scala
index a0d41424..f6079d77 100644
--- a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/util/UserApiClient.scala
+++ b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/util/UserApiClient.scala
@@ -18,36 +18,36 @@ object UserApiClient {
private val logger = LoggerFactory.getLogger(UserApiClient.getClass)
// User Service Configuration
- private val BASE_URL = "http://localhost:7001"
- private val AUTH_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImlkIjozMDg3LCJuYW1lIjoiRmFyYWJpIEFobWVkdWxsYWgiLCJzZXNzaW9uX2lkIjoyMzAyMywib3JnYW5pemF0aW9uX2lkcyI6WyI2NyJdLCJvcmdhbml6YXRpb25fY29kZXMiOlsiYnJhY19nYmwiXSwidGVuYW50X2NvZGUiOiJicmFjIiwib3JnYW5pemF0aW9ucyI6W3siaWQiOjY3LCJuYW1lIjoiQlJBQyBHQkwgb3JnIiwiY29kZSI6ImJyYWNfZ2JsIiwiZGVzY3JpcHRpb24iOiJCUkFDIEdCTCBvcmciLCJzdGF0dXMiOiJBQ1RJVkUiLCJyZWxhdGVkX29yZ3MiOm51bGwsInRlbmFudF9jb2RlIjoiYnJhYyIsIm1ldGEiOm51bGwsImNyZWF0ZWRfYnkiOm51bGwsInVwZGF0ZWRfYnkiOjEsInJvbGVzIjpbeyJpZCI6MjEzLCJ0aXRsZSI6InNlc3Npb25fbWFuYWdlciIsImxhYmVsIjoiTGlua2FnZSBDaGFtcGlvbiIsInVzZXJfdHlwZSI6MCwic3RhdHVzIjoiQUNUSVZFIiwib3JnYW5pemF0aW9uX2lkIjo2NywidmlzaWJpbGl0eSI6IlBVQkxJQyIsInRlbmFudF9jb2RlIjoiYnJhYyIsInRyYW5zbGF0aW9ucyI6bnVsbH1dfV19LCJpYXQiOjE3NjkwNjA4NTMsImV4cCI6MTc2OTE0NzI1M30.CPTelpSKG7wHA7WccMMwVI7rk0QajCt_Baf1rh0vCcw"
+ private val BASE_URL = "http://172.132.44.221:7001"
+ private val AUTH_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImlkIjozMDg3LCJuYW1lIjoiZmFyYWJpIFVwZGF0ZWQgdHdvIiwic2Vzc2lvbl9pZCI6MjMwMjgsIm9yZ2FuaXphdGlvbl9pZHMiOlsiNjciXSwib3JnYW5pemF0aW9uX2NvZGVzIjpbImJyYWNfZ2JsIl0sInRlbmFudF9jb2RlIjoiYnJhYyIsIm9yZ2FuaXphdGlvbnMiOlt7ImlkIjo2NywibmFtZSI6IkJSQUMgR0JMIG9yZyIsImNvZGUiOiJicmFjX2dibCIsImRlc2NyaXB0aW9uIjoiQlJBQyBHQkwgb3JnIiwic3RhdHVzIjoiQUNUSVZFIiwicmVsYXRlZF9vcmdzIjpudWxsLCJ0ZW5hbnRfY29kZSI6ImJyYWMiLCJtZXRhIjpudWxsLCJjcmVhdGVkX2J5IjpudWxsLCJ1cGRhdGVkX2J5IjoxLCJyb2xlcyI6W3siaWQiOjIxMywidGl0bGUiOiJzZXNzaW9uX21hbmFnZXIiLCJsYWJlbCI6IkxpbmthZ2UgQ2hhbXBpb24iLCJ1c2VyX3R5cGUiOjAsInN0YXR1cyI6IkFDVElWRSIsIm9yZ2FuaXphdGlvbl9pZCI6NjcsInZpc2liaWxpdHkiOiJQVUJMSUMiLCJ0ZW5hbnRfY29kZSI6ImJyYWMiLCJ0cmFuc2xhdGlvbnMiOm51bGx9XX1dfSwiaWF0IjoxNzY5NzUxNjcxLCJleHAiOjE3Njk4MzgwNzF9.wJpLStvXoU81vGQtIijs867BC7QJWO2F1w5F8W1X8-8"
println(s"[UserApiClient] Initialized with BASE_URL: $BASE_URL")
println(s"[UserApiClient] AUTH_TOKEN configured: ${if (AUTH_TOKEN.nonEmpty) "***" else "NOT SET"}")
/**
- * Patch user profile data for a student
+ * Patch user profile data for a user
*
- * @param studentId The student ID to update
+ * @param id The user ID to update
* @param profileData JSON object containing profile data (e.g., {"profile": {"phone": "...", "gender": "..."}})
* @return Success(true) if update successful, Failure(exception) otherwise
*/
- def patchProfile(studentId: String, profileData: Js.Obj): Try[Boolean] = {
- if (studentId == null || studentId.trim.isEmpty) {
- val error = new IllegalArgumentException("studentId cannot be null or empty")
- logger.error("[UserApiClient] studentId is null or empty", error)
- println(s"[UserApiClient] ERROR: studentId is null or empty")
+ def patchProfile(id: String, profileData: Js.Obj): Try[Boolean] = {
+ if (id == null || id.trim.isEmpty) {
+ val error = new IllegalArgumentException("id cannot be null or empty")
+ logger.error("[UserApiClient] id is null or empty", error)
+ println(s"[UserApiClient] ERROR: id is null or empty")
return Failure(error)
}
try {
val url = s"$BASE_URL/user/v1/user/update"
- // Merge studentId with profileData into a single payload
- // The API expects the payload directly, so we'll include studentId and merge profile fields
+ // Merge id with profileData into a single payload
+ // The API expects the payload directly, so we'll include id and merge profile fields
val payloadObj = Obj()
- // Add studentId to payload to identify which user to update
- payloadObj.value("id") = studentId
+ // Add id to payload to identify which user to update
+ payloadObj.value("id") = id
// Merge profile fields from profileData into payload
val profileObj = profileData.value.get("profile")
@@ -75,29 +75,32 @@ object UserApiClient {
println(s"[UserApiClient] Request headers: X-auth-token=***, Content-Type=application/json")
+ // Use check = false to prevent RequestFailedException from being thrown for non-2xx status codes
+ // This allows us to handle error responses gracefully
val response = requests.patch(
url,
data = jsonPayload,
- headers = headers
+ headers = headers,
+ check = false
)
println(s"[UserApiClient] Response status code: ${response.statusCode}")
println(s"[UserApiClient] Response body: ${response.text}")
if (response.statusCode >= 200 && response.statusCode < 300) {
- println(s"[UserApiClient] SUCCESS: Profile updated for studentId=$studentId")
- logger.info(s"[UserApiClient] Successfully updated profile for studentId=$studentId")
+ println(s"[UserApiClient] SUCCESS: Profile updated for id=$id")
+ logger.info(s"[UserApiClient] Successfully updated profile for id=$id")
Success(true)
} else {
val error = new Exception(s"User Service API returned status ${response.statusCode}: ${response.text}")
- logger.error(s"[UserApiClient] API error for studentId=$studentId: ${error.getMessage}", error)
+ logger.error(s"[UserApiClient] API error for id=$id: ${error.getMessage}", error)
println(s"[UserApiClient] ERROR: API returned status ${response.statusCode}: ${response.text}")
Failure(error)
}
} catch {
case e: Exception =>
- logger.error(s"[UserApiClient] Exception while patching profile for studentId=$studentId: ${e.getMessage}", e)
+ logger.error(s"[UserApiClient] Exception while patching profile for id=$id: ${e.getMessage}", e)
println(s"[UserApiClient] EXCEPTION: ${e.getMessage}")
e.printStackTrace()
Failure(e)
From a8df0d341259598c7e31defbae231e01dc3b056e Mon Sep 17 00:00:00 2001
From: Komal Mane
Date: Mon, 2 Feb 2026 15:36:05 +0530
Subject: [PATCH 04/22] Update config.env
---
Documentation/Docker-setup/config.env | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Documentation/Docker-setup/config.env b/Documentation/Docker-setup/config.env
index a4e9b4d0..203863f2 100644
--- a/Documentation/Docker-setup/config.env
+++ b/Documentation/Docker-setup/config.env
@@ -1,11 +1,11 @@
# Postgres
POSTGRES_USER=postgres
-POSTGRES_PASSWORD=1234
+POSTGRES_PASSWORD=password
POSTGRES_PORT=5432 #use this for docker
POSTGRES_HOST=postgres #use this for docker
# PGADMIN
-PGADMIN_DEFAULT_EMAIL=komal_m@tekditechnologies.com
+PGADMIN_DEFAULT_EMAIL=admin@example.com
PGADMIN_DEFAULT_PASSWORD=admin
# METABASE
From 999fe0aa85ffc7ed1f9ba003bee2f51f98b438a3 Mon Sep 17 00:00:00 2001
From: Komal Mane
Date: Mon, 2 Feb 2026 15:39:42 +0530
Subject: [PATCH 05/22] Update docker-compose.yml
---
Documentation/Docker-setup/docker-compose.yml | 23 +++++++++----------
1 file changed, 11 insertions(+), 12 deletions(-)
diff --git a/Documentation/Docker-setup/docker-compose.yml b/Documentation/Docker-setup/docker-compose.yml
index db07c97f..ba68548b 100644
--- a/Documentation/Docker-setup/docker-compose.yml
+++ b/Documentation/Docker-setup/docker-compose.yml
@@ -78,13 +78,13 @@ services:
restart: always
container_name: postgres
environment:
- POSTGRES_USER: postgres
- POSTGRES_PASSWORD: M3TabAse#321
- POSTGRES_DB: metabase
+ POSTGRES_USER: ${POSTGRES_USER}
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
+ POSTGRES_DB: ${POSTGRES_DB}
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- - "5533:5432"
+ - "5432:5432"
networks:
- elevate_net
@@ -111,14 +111,13 @@ services:
restart: always
container_name: metabase
environment:
- MB_DB_TYPE: "postgres"
- MB_DB_DBNAME: "metabase_elevate"
- MB_DB_PORT: "5432"
- MB_DB_USER: "postgres"
-# MB_DB_PASS: "M3TabAse#321"
- MB_DB_PASS: "1234"
- MB_DB_HOST: "172.132.44.221"
- MB_API_KEY: "9c2a8e4b-6f1d-3c5d-7e8f-1a2b3c4d5e6f"
+ MB_DB_TYPE: ${MB_DB_TYPE}
+ MB_DB_DBNAME: ${POSTGRES_DB}
+ MB_DB_PORT: ${POSTGRES_PORT}
+ MB_DB_USER: ${POSTGRES_USER}
+ MB_DB_PASS: ${POSTGRES_PASSWORD}
+ MB_DB_HOST: ${POSTGRES_HOST}
+ MB_API_KEY: ${MB_API_KEY}
ports:
- "3000:3000"
depends_on:
From 0a5d37ca552b0970a8ee82b7ffa0d0d8b5c7dde6 Mon Sep 17 00:00:00 2001
From: Komal Mane
Date: Mon, 2 Feb 2026 15:40:23 +0530
Subject: [PATCH 06/22] Update base-config.conf
---
jobs-core/src/main/resources/base-config.conf | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/jobs-core/src/main/resources/base-config.conf b/jobs-core/src/main/resources/base-config.conf
index 55c2f7e0..9a1fac4f 100644
--- a/jobs-core/src/main/resources/base-config.conf
+++ b/jobs-core/src/main/resources/base-config.conf
@@ -1,6 +1,6 @@
kafka {
- broker-servers = "kafka:29092"
- zookeeper = "zookeeper:2181"
+ broker-servers = "10.148.0.38:9092"
+ zookeeper = "10.148.0.38:2181"
}
job {
From 19c964b17a131d5b3715d9ac61e12e85e5346c7b Mon Sep 17 00:00:00 2001
From: Komal Mane
Date: Mon, 2 Feb 2026 15:41:34 +0530
Subject: [PATCH 07/22] Update BaseJobConfig.scala
---
.../src/main/scala/org/shikshalokam/job/BaseJobConfig.scala | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/jobs-core/src/main/scala/org/shikshalokam/job/BaseJobConfig.scala b/jobs-core/src/main/scala/org/shikshalokam/job/BaseJobConfig.scala
index cf612ab3..f747af5e 100644
--- a/jobs-core/src/main/scala/org/shikshalokam/job/BaseJobConfig.scala
+++ b/jobs-core/src/main/scala/org/shikshalokam/job/BaseJobConfig.scala
@@ -35,7 +35,7 @@ class BaseJobConfig(val config: Config, val jobName: String) extends Serializabl
def kafkaConsumerProperties: Properties = {
val properties = new Properties()
- properties.setProperty("bootstrap.servers", "kafka:29092")
+ properties.setProperty("bootstrap.servers", "kafkaBrokerServers")
properties.setProperty("group.id", groupId)
properties.setProperty(ConsumerConfig.ISOLATION_LEVEL_CONFIG, "read_committed")
From e261e651889f87a92eda3e7e715d416742ec7a8d Mon Sep 17 00:00:00 2001
From: Komal Mane
Date: Mon, 2 Feb 2026 15:42:13 +0530
Subject: [PATCH 08/22] Update BaseJobConfig.scala
---
.../src/main/scala/org/shikshalokam/job/BaseJobConfig.scala | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/jobs-core/src/main/scala/org/shikshalokam/job/BaseJobConfig.scala b/jobs-core/src/main/scala/org/shikshalokam/job/BaseJobConfig.scala
index f747af5e..4b28e0eb 100644
--- a/jobs-core/src/main/scala/org/shikshalokam/job/BaseJobConfig.scala
+++ b/jobs-core/src/main/scala/org/shikshalokam/job/BaseJobConfig.scala
@@ -35,7 +35,7 @@ class BaseJobConfig(val config: Config, val jobName: String) extends Serializabl
def kafkaConsumerProperties: Properties = {
val properties = new Properties()
- properties.setProperty("bootstrap.servers", "kafkaBrokerServers")
+ properties.setProperty("bootstrap.servers", kafkaBrokerServers)
properties.setProperty("group.id", groupId)
properties.setProperty(ConsumerConfig.ISOLATION_LEVEL_CONFIG, "read_committed")
From ea4df41feb537190c56303e3a7dc2dddc3046527 Mon Sep 17 00:00:00 2001
From: Komal Mane
Date: Mon, 2 Feb 2026 15:43:41 +0530
Subject: [PATCH 09/22] Update BaseJobConfig.scala
---
.../scala/org/shikshalokam/job/BaseJobConfig.scala | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/jobs-core/src/main/scala/org/shikshalokam/job/BaseJobConfig.scala b/jobs-core/src/main/scala/org/shikshalokam/job/BaseJobConfig.scala
index 4b28e0eb..c5bd6136 100644
--- a/jobs-core/src/main/scala/org/shikshalokam/job/BaseJobConfig.scala
+++ b/jobs-core/src/main/scala/org/shikshalokam/job/BaseJobConfig.scala
@@ -41,9 +41,9 @@ class BaseJobConfig(val config: Config, val jobName: String) extends Serializabl
// Add timeout configurations to handle connection issues better
// Default timeout is 30 seconds, increase to 60 seconds for better reliability
- properties.setProperty(ConsumerConfig.REQUEST_TIMEOUT_MS_CONFIG, "60000")
- properties.setProperty(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, "30000")
- properties.setProperty(ConsumerConfig.METADATA_MAX_AGE_CONFIG, "300000")
+ // properties.setProperty(ConsumerConfig.REQUEST_TIMEOUT_MS_CONFIG, "60000")
+ // properties.setProperty(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, "30000")
+ // properties.setProperty(ConsumerConfig.METADATA_MAX_AGE_CONFIG, "300000")
kafkaAutoOffsetReset.map {
properties.setProperty("auto.offset.reset", _)
@@ -59,8 +59,8 @@ class BaseJobConfig(val config: Config, val jobName: String) extends Serializabl
properties.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, "snappy")
// Add timeout configurations for producer
- properties.setProperty(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG, "60000")
- properties.setProperty(ProducerConfig.DELIVERY_TIMEOUT_MS_CONFIG, "120000")
+ // properties.setProperty(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG, "60000")
+ // properties.setProperty(ProducerConfig.DELIVERY_TIMEOUT_MS_CONFIG, "120000")
properties
}
From dfc023b49b0fe72bc8020d061ccdd904025ca6bb Mon Sep 17 00:00:00 2001
From: Komal Mane
Date: Mon, 2 Feb 2026 15:45:09 +0530
Subject: [PATCH 10/22] Update data-loader.sh
---
metabase-jobs/config-data-loader/data-loader.sh | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/metabase-jobs/config-data-loader/data-loader.sh b/metabase-jobs/config-data-loader/data-loader.sh
index d1dbda1d..76753f68 100755
--- a/metabase-jobs/config-data-loader/data-loader.sh
+++ b/metabase-jobs/config-data-loader/data-loader.sh
@@ -1,9 +1,9 @@
#!/bin/bash
# Database connection parameters
-DB_NAME="elevate_data"
+DB_NAME="postgres"
DB_USER="postgres"
-DB_PASSWORD="1234"
+DB_PASSWORD="postgres"
DB_HOST="localhost"
DB_PORT="5432"
TABLE_NAME="local_report_config"
@@ -75,7 +75,7 @@ process_folders() {
}
# Main folder path
-MAIN_FOLDER="/home/ttpl-rt-221/elevate/data-pipeline/metabase-jobs/config-data-loader/projectJson"
+MAIN_FOLDER="/home/user2/Documents/elevate/data-pipeline/metabase-jobs/config-data-loader/projectJson"
# Create the table and process folders
create_table
From 1a8998c3b4828f18dbf72f85be15bed681380c75 Mon Sep 17 00:00:00 2001
From: Komal Mane
Date: Mon, 2 Feb 2026 15:46:03 +0530
Subject: [PATCH 11/22] Update metabase-project-dashboard.conf
---
.../src/main/resources/metabase-project-dashboard.conf | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/metabase-jobs/project-dashboard-creator/src/main/resources/metabase-project-dashboard.conf b/metabase-jobs/project-dashboard-creator/src/main/resources/metabase-project-dashboard.conf
index 18e1b138..c8e64da9 100644
--- a/metabase-jobs/project-dashboard-creator/src/main/resources/metabase-project-dashboard.conf
+++ b/metabase-jobs/project-dashboard-creator/src/main/resources/metabase-project-dashboard.conf
@@ -12,8 +12,8 @@ postgres{
host = "localhost"
port = "5432"
username = "postgres"
- password = "1234"
- database = "elevate_data"
+ password = "postgres"
+ database = "test"
metabaseDb = "metabase"
tables = {
solutionsTable = ${job.env}"_solutions"
From ccafdaea62581e7f8a3e2119e42f2d4532b5742f Mon Sep 17 00:00:00 2001
From: Komal Mane
Date: Mon, 2 Feb 2026 15:47:50 +0530
Subject: [PATCH 12/22] Update test.conf
---
.../src/test/resources/test.conf | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/metabase-jobs/project-dashboard-creator/src/test/resources/test.conf b/metabase-jobs/project-dashboard-creator/src/test/resources/test.conf
index abde8313..0452ecff 100644
--- a/metabase-jobs/project-dashboard-creator/src/test/resources/test.conf
+++ b/metabase-jobs/project-dashboard-creator/src/test/resources/test.conf
@@ -13,9 +13,9 @@ postgres{
host = "localhost"
port = "5432"
username = "postgres"
- password = "1234"
- database = "elevate_data"
- metabaseDb = "metabase_elevate"
+ password = "postgres"
+ database = "test"
+ metabaseDb = "metabase"
tables = {
solutionsTable = ${job.env}"_solutions"
projectsTable = ${job.env}"_projects"
@@ -27,10 +27,10 @@ postgres{
metabase {
url = "http://localhost:3000/api"
- username = "komal@yopmail.com"
- password = "pw4komal"
- database = "Postgres"
+ username = "vivek@shikshalokam.org"
+ password = "Test@1234"
+ database = "test"
domainName = "http://localhost:3000/dashboard/"
- metabaseApiKey = "9c2a8e4b-6f1d-3c5d-7e8f-1a2b3c4d5e6f"
+ metabaseApiKey = "d3f4ult-api-key"
evidenceBaseUrl = "https://TESTING="
}
From 8d8f2549b1ef388933e0cf71686c002add560c6b Mon Sep 17 00:00:00 2001
From: Komal Mane
Date: Mon, 2 Feb 2026 15:48:33 +0530
Subject: [PATCH 13/22] Update project-stream.conf
---
.../src/main/resources/project-stream.conf | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/stream-jobs/project-stream-processor/src/main/resources/project-stream.conf b/stream-jobs/project-stream-processor/src/main/resources/project-stream.conf
index c5455290..f06ff30c 100644
--- a/stream-jobs/project-stream-processor/src/main/resources/project-stream.conf
+++ b/stream-jobs/project-stream-processor/src/main/resources/project-stream.conf
@@ -15,8 +15,8 @@ task {
postgres{
host = "localhost"
port = "5432"
- username = "postgres"
- password = "1234"
+ password = "postgres"
+ database = "test"
database = "elevate_data"
tables = {
solutionsTable = ${job.env}"_solutions"
From 7844332002d96e5edf2d24e6558c3625911a581a Mon Sep 17 00:00:00 2001
From: Komal Mane
Date: Mon, 2 Feb 2026 15:49:42 +0530
Subject: [PATCH 14/22] Update project-stream.conf
---
.../src/main/resources/project-stream.conf | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/stream-jobs/project-stream-processor/src/main/resources/project-stream.conf b/stream-jobs/project-stream-processor/src/main/resources/project-stream.conf
index f06ff30c..d685516b 100644
--- a/stream-jobs/project-stream-processor/src/main/resources/project-stream.conf
+++ b/stream-jobs/project-stream-processor/src/main/resources/project-stream.conf
@@ -15,9 +15,9 @@ task {
postgres{
host = "localhost"
port = "5432"
+ username = "postgres"
password = "postgres"
database = "test"
- database = "elevate_data"
tables = {
solutionsTable = ${job.env}"_solutions"
projectsTable = ${job.env}"_projects"
From 8216377abb2abdae0828c7b15bdc59a0108264a9 Mon Sep 17 00:00:00 2001
From: Komal Mane
Date: Mon, 2 Feb 2026 15:50:21 +0530
Subject: [PATCH 15/22] Update test.conf
---
.../project-stream-processor/src/test/resources/test.conf | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/stream-jobs/project-stream-processor/src/test/resources/test.conf b/stream-jobs/project-stream-processor/src/test/resources/test.conf
index 4b41f13f..eff4dab0 100644
--- a/stream-jobs/project-stream-processor/src/test/resources/test.conf
+++ b/stream-jobs/project-stream-processor/src/test/resources/test.conf
@@ -15,8 +15,8 @@ postgres{
host = "localhost"
port = "5432"
username = "postgres"
- password = "1234"
- database = "elevate_data"
+ password = "postgres"
+ database = "test"
tables = {
solutionsTable = ${job.env}"_solutions"
projectsTable = ${job.env}"_projects"
From 09b28e9135c369e945a23dc761132d63137902b0 Mon Sep 17 00:00:00 2001
From: Komal Mane
Date: Mon, 2 Feb 2026 15:53:40 +0530
Subject: [PATCH 16/22] Update user-mapping-stream.conf
---
.../src/main/resources/user-mapping-stream.conf | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/stream-jobs/user-mapping-stream-processor/src/main/resources/user-mapping-stream.conf b/stream-jobs/user-mapping-stream-processor/src/main/resources/user-mapping-stream.conf
index c141b59a..e67e2f2d 100644
--- a/stream-jobs/user-mapping-stream-processor/src/main/resources/user-mapping-stream.conf
+++ b/stream-jobs/user-mapping-stream-processor/src/main/resources/user-mapping-stream.conf
@@ -6,7 +6,7 @@ kafka {
# broker-servers = "localhost:9092" # For local development outside Docker
# broker-servers = "your-kafka-host:9092" # For remote Kafka instance
- input.topic = "dev.observation-submission"
+ input.topic = "brac.observation.submission.dev"
groupId = "dev.users"
output.topic = "sl-metabase-user-dashboard-dev"
output.mentoring.topic = "sl-metabase-mentoring-dashboard-dev"
@@ -22,8 +22,8 @@ postgres{
host = "localhost"
port = "5432"
username = "postgres"
- password = "1234"
- database = "elevate_data"
+ password = "Test@123"
+ database = "test"
tables = {
userMetrics = ${job.env}"_user_metrics"
dashboardMetadataTable = ${job.env}"_dashboard_metadata"
From 6854c49032e60bdf459c55dc8215494a05cad7f2 Mon Sep 17 00:00:00 2001
From: Komal Mane
Date: Mon, 2 Feb 2026 15:55:18 +0530
Subject: [PATCH 17/22] Update ObservationEvent.scala
---
.../stream/processor/domain/ObservationEvent.scala | 11 -----------
1 file changed, 11 deletions(-)
diff --git a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/domain/ObservationEvent.scala b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/domain/ObservationEvent.scala
index d0cfe1d0..f27bae7e 100644
--- a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/domain/ObservationEvent.scala
+++ b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/domain/ObservationEvent.scala
@@ -9,17 +9,6 @@ import scala.collection.JavaConverters._
/**
* Case class to represent observation submission events from Kafka
*
- * Sample event structure:
- * {
- * "eventType": "observation-submitted",
- * "id": 3088,
- * "organizationId": 1,
- * "observationData": {
- * "name": "Carol Miranda Updated Two",
- * "about": "admin Update",
- * "dob": "22-12-1990"
- * }
- * }
*/
class ObservationEvent(eventMap: java.util.Map[String, Any], partition: Int, offset: Long)
extends JobRequest(eventMap, partition, offset) {
From 0bf2a6844dc8da6c2c173860cb36abdf1f029f17 Mon Sep 17 00:00:00 2001
From: Komal Mane
Date: Mon, 2 Feb 2026 16:00:49 +0530
Subject: [PATCH 18/22] Update UserMappingStreamFunction.scala
---
.../functions/UserMappingStreamFunction.scala | 34 ++-----------------
1 file changed, 3 insertions(+), 31 deletions(-)
diff --git a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/functions/UserMappingStreamFunction.scala b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/functions/UserMappingStreamFunction.scala
index 8dec4698..a982c1c4 100644
--- a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/functions/UserMappingStreamFunction.scala
+++ b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/functions/UserMappingStreamFunction.scala
@@ -25,8 +25,6 @@ class UserMappingStreamFunction(config: UserMappingStreamConfig)(implicit val ma
override def open(parameters: Configuration): Unit = {
super.open(parameters)
- println("[UserMappingStreamFunction] Initializing...")
- println("[UserMappingStreamFunction] FieldMapper mappings loaded")
FieldMapper.getMappings.foreach { case (source, target) =>
println(s"[UserMappingStreamFunction] Mapping: $source -> $target")
}
@@ -34,7 +32,6 @@ class UserMappingStreamFunction(config: UserMappingStreamConfig)(implicit val ma
override def close(): Unit = {
super.close()
- println("[UserMappingStreamFunction] Closing...")
}
/**
@@ -84,19 +81,8 @@ class UserMappingStreamFunction(config: UserMappingStreamConfig)(implicit val ma
println(s"***************** Start Processing Observation Event *****************")
// Print all fields from the event map
- println(s"[UserMappingStreamFunction] ========== ALL EVENT FIELDS ==========")
val eventMap = event.getMap()
- println(s"[UserMappingStreamFunction] Total top-level fields: ${eventMap.size()}")
- printAllFields(eventMap)
- println(s"[UserMappingStreamFunction] ========================================")
-
- // Print JSON representation
- println(s"[UserMappingStreamFunction] Full Event JSON:")
- println(s"[UserMappingStreamFunction] ${event.getJson()}")
-
- println(s"[UserMappingStreamFunction] Event Type: ${event.eventType}")
- println(s"[UserMappingStreamFunction] ID: ${event.id}")
- println(s"[UserMappingStreamFunction] Organization ID: ${event.organizationId}")
+ // printAllFields(eventMap)
// Update total events count metric
metrics.incCounter(config.totalEventsCount)
@@ -104,7 +90,6 @@ class UserMappingStreamFunction(config: UserMappingStreamConfig)(implicit val ma
// Validate event - ensure id is present
val id = event.id
if (id == null || id.trim.isEmpty) {
- println(s"[UserMappingStreamFunction] ERROR: id is null or empty, skipping event")
logger.error("[UserMappingStreamFunction] id is null or empty")
metrics.incCounter(config.skipCount)
return
@@ -115,27 +100,22 @@ class UserMappingStreamFunction(config: UserMappingStreamConfig)(implicit val ma
val userProfile = event.observationData
if (userProfile == null || userProfile.isEmpty) {
- println(s"[UserMappingStreamFunction] WARNING: userProfile is null or empty for id=$id, skipping event")
logger.warn(s"[UserMappingStreamFunction] userProfile is null or empty for id=$id")
metrics.incCounter(config.skipCount)
return
}
-
- println(s"[UserMappingStreamFunction] userProfile keys: ${userProfile.keySet().asScala.mkString(", ")}")
-
+
// Transform userProfile data to profile format using FieldMapper
// FieldMapper will:
// 1. Only map allowed fields (name, username, dob, phoneCode, about, preferredLanguage, tenantCode, meta)
// 2. Skip fields that are null, empty, or missing
// 3. Return a profile object with only non-empty fields
- println(s"[UserMappingStreamFunction] Transforming userProfile to profile format...")
val profileData = FieldMapper.transform(userProfile)
// Check if profile data contains any valid (non-empty) fields
// Only call API if at least one field is eligible for update
val profileObj = profileData.value.get("profile")
if (profileObj.isEmpty) {
- println(s"[UserMappingStreamFunction] WARNING: No profile object in transformed data for id=$id, skipping API call")
logger.warn(s"[UserMappingStreamFunction] No profile object in transformed data for id=$id")
metrics.incCounter(config.skipCount)
return
@@ -143,30 +123,23 @@ class UserMappingStreamFunction(config: UserMappingStreamConfig)(implicit val ma
val profileFields = profileObj.get.asInstanceOf[ujson.Obj].value
if (profileFields.isEmpty) {
- println(s"[UserMappingStreamFunction] INFO: No valid (non-empty) fields to update for id=$id. All fields were null, empty, or missing. Skipping API call.")
logger.info(s"[UserMappingStreamFunction] No valid fields to update for id=$id - all fields were empty/null/missing")
metrics.incCounter(config.skipCount)
return
}
-
- println(s"[UserMappingStreamFunction] Found ${profileFields.size} valid field(s) to update: ${profileFields.keys.mkString(", ")}")
-
+
// Call User Service API to patch the profile
// Only non-empty fields will be included in the update request
- println(s"[UserMappingStreamFunction] Calling UserApiClient.patchProfile for id=$id...")
UserApiClient.patchProfile(id, profileData) match {
case Success(true) =>
- println(s"[UserMappingStreamFunction] SUCCESS: Profile updated for id=$id with ${profileFields.size} field(s)")
logger.info(s"[UserMappingStreamFunction] Successfully updated profile for id=$id with fields: ${profileFields.keys.mkString(", ")}")
metrics.incCounter(config.successCount)
case Success(false) =>
- println(s"[UserMappingStreamFunction] FAILED: API returned success=false for id=$id")
logger.warn(s"[UserMappingStreamFunction] API returned success=false for id=$id")
metrics.incCounter(config.skipCount)
case Failure(exception) =>
- println(s"[UserMappingStreamFunction] FAILED: Could not update profile for id=$id: ${exception.getMessage}")
logger.error(s"[UserMappingStreamFunction] Failed to update profile for id=$id", exception)
metrics.incCounter(config.skipCount)
// Log error but continue processing - don't crash the task for API failures
@@ -177,7 +150,6 @@ class UserMappingStreamFunction(config: UserMappingStreamConfig)(implicit val ma
} catch {
case e: Exception =>
- println(s"[UserMappingStreamFunction] EXCEPTION: Error processing observation event: ${e.getMessage}")
logger.error(s"[UserMappingStreamFunction] Exception processing observation event", e)
e.printStackTrace()
metrics.incCounter(config.skipCount)
From a642b3dd6a2756ef482bd76dbccfcc280afc76ed Mon Sep 17 00:00:00 2001
From: Komal Mane
Date: Mon, 2 Feb 2026 16:11:50 +0530
Subject: [PATCH 19/22] Update UserMappingStreamTask.scala
---
.../task/UserMappingStreamTask.scala | 32 +++++++++----------
1 file changed, 16 insertions(+), 16 deletions(-)
diff --git a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/task/UserMappingStreamTask.scala b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/task/UserMappingStreamTask.scala
index 20560560..f0b61e22 100644
--- a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/task/UserMappingStreamTask.scala
+++ b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/task/UserMappingStreamTask.scala
@@ -65,13 +65,13 @@ object UserMappingStreamTask {
// Log Kafka configuration for debugging
val brokerServers = userMappingStreamConfig.kafkaBrokerServers
- println(s"[UserMappingStreamTask] ========================================")
- println(s"[UserMappingStreamTask] Kafka Configuration:")
- println(s"[UserMappingStreamTask] Broker Servers: $brokerServers")
- println(s"[UserMappingStreamTask] Group ID: ${userMappingStreamConfig.groupId}")
- println(s"[UserMappingStreamTask] Input Topic: ${userMappingStreamConfig.inputTopic}")
- println(s"[UserMappingStreamTask] Output Topic: ${userMappingStreamConfig.outputTopic}")
- println(s"[UserMappingStreamTask] ========================================")
+ // println(s"[UserMappingStreamTask] ========================================")
+ // println(s"[UserMappingStreamTask] Kafka Configuration:")
+ // println(s"[UserMappingStreamTask] Broker Servers: $brokerServers")
+ // println(s"[UserMappingStreamTask] Group ID: ${userMappingStreamConfig.groupId}")
+ // println(s"[UserMappingStreamTask] Input Topic: ${userMappingStreamConfig.inputTopic}")
+ // println(s"[UserMappingStreamTask] Output Topic: ${userMappingStreamConfig.outputTopic}")
+ // println(s"[UserMappingStreamTask] ========================================")
// Validate Kafka broker servers configuration
if (brokerServers == null || brokerServers.trim.isEmpty) {
@@ -88,17 +88,17 @@ object UserMappingStreamTask {
}
// Provide troubleshooting information
- println(s"[UserMappingStreamTask] Troubleshooting:")
- println(s"[UserMappingStreamTask] If you see 'TimeoutException: Timeout expired while fetching topic metadata':")
- println(s"[UserMappingStreamTask] 1. Verify Kafka is running: Check if Kafka broker is accessible at $brokerServers")
- println(s"[UserMappingStreamTask] 2. Test connection: Try 'telnet ' or 'nc -zv '")
- println(s"[UserMappingStreamTask] 3. Check firewall: Ensure port is not blocked")
- println(s"[UserMappingStreamTask] 4. Verify address: Confirm the broker address is correct in your config")
- println(s"[UserMappingStreamTask] 5. Check topics exist: Verify topics '${userMappingStreamConfig.inputTopic}' and '${userMappingStreamConfig.outputTopic}' exist")
- println(s"[UserMappingStreamTask]")
+ // println(s"[UserMappingStreamTask] Troubleshooting:")
+ // println(s"[UserMappingStreamTask] If you see 'TimeoutException: Timeout expired while fetching topic metadata':")
+ // println(s"[UserMappingStreamTask] 1. Verify Kafka is running: Check if Kafka broker is accessible at $brokerServers")
+ // println(s"[UserMappingStreamTask] 2. Test connection: Try 'telnet ' or 'nc -zv '")
+ // println(s"[UserMappingStreamTask] 3. Check firewall: Ensure port is not blocked")
+ // println(s"[UserMappingStreamTask] 4. Verify address: Confirm the broker address is correct in your config")
+ // println(s"[UserMappingStreamTask] 5. Check topics exist: Verify topics '${userMappingStreamConfig.inputTopic}' and '${userMappingStreamConfig.outputTopic}' exist")
+ // println(s"[UserMappingStreamTask]")
val kafkaUtil = new FlinkKafkaConnector(userMappingStreamConfig)
val task = new UserMappingStreamTask(userMappingStreamConfig, kafkaUtil)
task.process()
}
-}
\ No newline at end of file
+}
From 2c7e2aa1d4cf3f35246409164bd0c20d51cf4f35 Mon Sep 17 00:00:00 2001
From: Komal Mane
Date: Mon, 2 Feb 2026 16:14:03 +0530
Subject: [PATCH 20/22] Update FieldMapper.scala
---
.../user/mapping/stream/processor/util/FieldMapper.scala | 9 +--------
1 file changed, 1 insertion(+), 8 deletions(-)
diff --git a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/util/FieldMapper.scala b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/util/FieldMapper.scala
index 680e9f58..0111de6e 100644
--- a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/util/FieldMapper.scala
+++ b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/util/FieldMapper.scala
@@ -23,7 +23,6 @@ object FieldMapper {
// Load field mappings from config file
private val mappings: Map[String, String] = loadMappings()
- println(s"[FieldMapper] Loaded ${mappings.size} field mappings")
mappings.foreach { case (source, target) =>
println(s"[FieldMapper] Mapping: $source -> $target")
}
@@ -38,7 +37,6 @@ object FieldMapper {
val config = ConfigFactory.parseResources("field-mappings.conf")
if (config.isEmpty) {
logger.warn("[FieldMapper] field-mappings.conf is empty or not found")
- println("[FieldMapper] WARNING: field-mappings.conf is empty or not found")
return Map.empty[String, String]
}
@@ -49,12 +47,10 @@ object FieldMapper {
(key, value)
}.toMap
- println(s"[FieldMapper] Successfully loaded ${configMap.size} mappings from field-mappings.conf")
configMap
} catch {
case e: Exception =>
logger.error(s"[FieldMapper] Failed to load field-mappings.conf: ${e.getMessage}", e)
- println(s"[FieldMapper] ERROR: Failed to load field-mappings.conf: ${e.getMessage}")
e.printStackTrace()
Map.empty[String, String]
}
@@ -177,7 +173,6 @@ object FieldMapper {
def transform(observationData: util.Map[String, Any]): Js.Obj = {
try {
println(s"[FieldMapper] Starting transformation of userProfile data")
- println(s"[FieldMapper] Input userProfile keys: ${observationData.keySet().asScala.mkString(", ")}")
val profileObj = Obj()
var mappedFieldsCount = 0
@@ -247,19 +242,17 @@ object FieldMapper {
} catch {
case e: Exception =>
logger.error(s"[FieldMapper] Error mapping field $sourceField: ${e.getMessage}", e)
- println(s"[FieldMapper] ERROR mapping field $sourceField: ${e.getMessage}")
}
}
// Return the result with "profile" as root
val result = Obj("profile" -> profileObj)
- println(s"[FieldMapper] Transformation complete. Mapped $mappedFieldsCount non-empty fields. Result: ${result.render()}")
+ // println(s"[FieldMapper] Transformation complete. Mapped $mappedFieldsCount non-empty fields. Result: ${result.render()}")
result
} catch {
case e: Exception =>
logger.error(s"[FieldMapper] Error during transformation: ${e.getMessage}", e)
- println(s"[FieldMapper] ERROR during transformation: ${e.getMessage}")
e.printStackTrace()
Obj("profile" -> Obj()) // Return empty profile on error
}
From a7dc7f2013b15ec9668709d071c8f34095ebcd8a Mon Sep 17 00:00:00 2001
From: Komal Mane
Date: Mon, 2 Feb 2026 16:15:36 +0530
Subject: [PATCH 21/22] Update UserApiClient.scala
---
.../stream/processor/util/UserApiClient.scala | 16 +++++-----------
1 file changed, 5 insertions(+), 11 deletions(-)
diff --git a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/util/UserApiClient.scala b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/util/UserApiClient.scala
index f6079d77..39caab58 100644
--- a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/util/UserApiClient.scala
+++ b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/util/UserApiClient.scala
@@ -21,8 +21,6 @@ object UserApiClient {
private val BASE_URL = "http://172.132.44.221:7001"
private val AUTH_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImlkIjozMDg3LCJuYW1lIjoiZmFyYWJpIFVwZGF0ZWQgdHdvIiwic2Vzc2lvbl9pZCI6MjMwMjgsIm9yZ2FuaXphdGlvbl9pZHMiOlsiNjciXSwib3JnYW5pemF0aW9uX2NvZGVzIjpbImJyYWNfZ2JsIl0sInRlbmFudF9jb2RlIjoiYnJhYyIsIm9yZ2FuaXphdGlvbnMiOlt7ImlkIjo2NywibmFtZSI6IkJSQUMgR0JMIG9yZyIsImNvZGUiOiJicmFjX2dibCIsImRlc2NyaXB0aW9uIjoiQlJBQyBHQkwgb3JnIiwic3RhdHVzIjoiQUNUSVZFIiwicmVsYXRlZF9vcmdzIjpudWxsLCJ0ZW5hbnRfY29kZSI6ImJyYWMiLCJtZXRhIjpudWxsLCJjcmVhdGVkX2J5IjpudWxsLCJ1cGRhdGVkX2J5IjoxLCJyb2xlcyI6W3siaWQiOjIxMywidGl0bGUiOiJzZXNzaW9uX21hbmFnZXIiLCJsYWJlbCI6IkxpbmthZ2UgQ2hhbXBpb24iLCJ1c2VyX3R5cGUiOjAsInN0YXR1cyI6IkFDVElWRSIsIm9yZ2FuaXphdGlvbl9pZCI6NjcsInZpc2liaWxpdHkiOiJQVUJMSUMiLCJ0ZW5hbnRfY29kZSI6ImJyYWMiLCJ0cmFuc2xhdGlvbnMiOm51bGx9XX1dfSwiaWF0IjoxNzY5NzUxNjcxLCJleHAiOjE3Njk4MzgwNzF9.wJpLStvXoU81vGQtIijs867BC7QJWO2F1w5F8W1X8-8"
- println(s"[UserApiClient] Initialized with BASE_URL: $BASE_URL")
- println(s"[UserApiClient] AUTH_TOKEN configured: ${if (AUTH_TOKEN.nonEmpty) "***" else "NOT SET"}")
/**
* Patch user profile data for a user
@@ -35,7 +33,6 @@ object UserApiClient {
if (id == null || id.trim.isEmpty) {
val error = new IllegalArgumentException("id cannot be null or empty")
logger.error("[UserApiClient] id is null or empty", error)
- println(s"[UserApiClient] ERROR: id is null or empty")
return Failure(error)
}
@@ -65,15 +62,15 @@ object UserApiClient {
val jsonPayload = payloadObj.render()
- println(s"[UserApiClient] PATCH Request to: $url")
- println(s"[UserApiClient] Request payload: $jsonPayload")
+ // println(s"[UserApiClient] PATCH Request to: $url")
+ // println(s"[UserApiClient] Request payload: $jsonPayload")
val headers = Map(
"X-auth-token" -> AUTH_TOKEN,
"Content-Type" -> "application/json"
)
- println(s"[UserApiClient] Request headers: X-auth-token=***, Content-Type=application/json")
+ // println(s"[UserApiClient] Request headers: X-auth-token=***, Content-Type=application/json")
// Use check = false to prevent RequestFailedException from being thrown for non-2xx status codes
// This allows us to handle error responses gracefully
@@ -84,24 +81,21 @@ object UserApiClient {
check = false
)
- println(s"[UserApiClient] Response status code: ${response.statusCode}")
- println(s"[UserApiClient] Response body: ${response.text}")
+ // println(s"[UserApiClient] Response status code: ${response.statusCode}")
+ // println(s"[UserApiClient] Response body: ${response.text}")
if (response.statusCode >= 200 && response.statusCode < 300) {
- println(s"[UserApiClient] SUCCESS: Profile updated for id=$id")
logger.info(s"[UserApiClient] Successfully updated profile for id=$id")
Success(true)
} else {
val error = new Exception(s"User Service API returned status ${response.statusCode}: ${response.text}")
logger.error(s"[UserApiClient] API error for id=$id: ${error.getMessage}", error)
- println(s"[UserApiClient] ERROR: API returned status ${response.statusCode}: ${response.text}")
Failure(error)
}
} catch {
case e: Exception =>
logger.error(s"[UserApiClient] Exception while patching profile for id=$id: ${e.getMessage}", e)
- println(s"[UserApiClient] EXCEPTION: ${e.getMessage}")
e.printStackTrace()
Failure(e)
}
From 9c7b8eb22cdc54c54ba78af6d765b33aeee6c4f7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E2=80=9CKomal?= <“komal_m@tekditechnologies.com”>
Date: Mon, 2 Feb 2026 17:18:48 +0530
Subject: [PATCH 22/22] Task #252259 feat: When the household profile
Observation is submitted, participant's profile should be updated
---
.../main/resources/user-mapping-stream.conf | 4 +++
.../functions/UserMappingStreamFunction.scala | 2 +-
.../task/UserMappingStreamConfig.scala | 4 +++
.../stream/processor/util/UserApiClient.scala | 26 +++++++++----------
4 files changed, 22 insertions(+), 14 deletions(-)
diff --git a/stream-jobs/user-mapping-stream-processor/src/main/resources/user-mapping-stream.conf b/stream-jobs/user-mapping-stream-processor/src/main/resources/user-mapping-stream.conf
index c141b59a..ae500ad7 100644
--- a/stream-jobs/user-mapping-stream-processor/src/main/resources/user-mapping-stream.conf
+++ b/stream-jobs/user-mapping-stream-processor/src/main/resources/user-mapping-stream.conf
@@ -30,3 +30,7 @@ postgres{
}
}
+userService {
+ baseUrl = "http://172.132.44.221:7001"
+ authToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImlkIjozMDg3LCJuYW1lIjoiZmFyYWJpIHVwZGF0ZWQgdmlhIGRhdGFwaXBsaW5lIiwic2Vzc2lvbl9pZCI6MjMwMjksIm9yZ2FuaXphdGlvbl9pZHMiOlsiNjciXSwib3JnYW5pemF0aW9uX2NvZGVzIjpbImJyYWNfZ2JsIl0sInRlbmFudF9jb2RlIjoiYnJhYyIsIm9yZ2FuaXphdGlvbnMiOlt7ImlkIjo2NywibmFtZSI6IkJSQUMgR0JMIG9yZyIsImNvZGUiOiJicmFjX2dibCIsImRlc2NyaXB0aW9uIjoiQlJBQyBHQkwgb3JnIiwic3RhdHVzIjoiQUNUSVZFIiwicmVsYXRlZF9vcmdzIjpudWxsLCJ0ZW5hbnRfY29kZSI6ImJyYWMiLCJtZXRhIjpudWxsLCJjcmVhdGVkX2J5IjpudWxsLCJ1cGRhdGVkX2J5IjoxLCJyb2xlcyI6W3siaWQiOjIxMywidGl0bGUiOiJzZXNzaW9uX21hbmFnZXIiLCJsYWJlbCI6IkxpbmthZ2UgQ2hhbXBpb24iLCJ1c2VyX3R5cGUiOjAsInN0YXR1cyI6IkFDVElWRSIsIm9yZ2FuaXphdGlvbl9pZCI6NjcsInZpc2liaWxpdHkiOiJQVUJMSUMiLCJ0ZW5hbnRfY29kZSI6ImJyYWMiLCJ0cmFuc2xhdGlvbnMiOm51bGx9XX1dfSwiaWF0IjoxNzcwMDMyMTIxLCJleHAiOjE3NzAxMTg1MjF9.T-NEvC4BzBIDjF-NHFmOIZlL9XDF6AwcLUllNCZSSJU"
+}
diff --git a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/functions/UserMappingStreamFunction.scala b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/functions/UserMappingStreamFunction.scala
index 8dec4698..7dff5964 100644
--- a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/functions/UserMappingStreamFunction.scala
+++ b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/functions/UserMappingStreamFunction.scala
@@ -154,7 +154,7 @@ class UserMappingStreamFunction(config: UserMappingStreamConfig)(implicit val ma
// Call User Service API to patch the profile
// Only non-empty fields will be included in the update request
println(s"[UserMappingStreamFunction] Calling UserApiClient.patchProfile for id=$id...")
- UserApiClient.patchProfile(id, profileData) match {
+ UserApiClient.patchProfile(id, profileData, config.userServiceBaseUrl, config.userServiceAuthToken) match {
case Success(true) =>
println(s"[UserMappingStreamFunction] SUCCESS: Profile updated for id=$id with ${profileFields.size} field(s)")
logger.info(s"[UserMappingStreamFunction] Successfully updated profile for id=$id with fields: ${profileFields.keys.mkString(", ")}")
diff --git a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/task/UserMappingStreamConfig.scala b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/task/UserMappingStreamConfig.scala
index 2d2ed2db..48db4907 100644
--- a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/task/UserMappingStreamConfig.scala
+++ b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/task/UserMappingStreamConfig.scala
@@ -50,6 +50,10 @@ class UserMappingStreamConfig(override val config: Config) extends BaseJobConfig
val userMetrics: String = config.getString("postgres.tables.userMetrics")
val dashboardMetadata: String = config.getString("postgres.tables.dashboardMetadataTable")
+ // User Service API Configuration
+ val userServiceBaseUrl: String = config.getString("userService.baseUrl")
+ val userServiceAuthToken: String = config.getString("userService.authToken")
+
val createTenantUserMetadataTable: String =
s"""
|CREATE TABLE IF NOT EXISTS @tenantTable (
diff --git a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/util/UserApiClient.scala b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/util/UserApiClient.scala
index f6079d77..0fb620ea 100644
--- a/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/util/UserApiClient.scala
+++ b/stream-jobs/user-mapping-stream-processor/src/main/scala/org/shikshalokam/job/user/mapping/stream/processor/util/UserApiClient.scala
@@ -9,7 +9,7 @@ import scala.util.{Failure, Success, Try}
/**
* HTTP client for making PATCH requests to User Service API
*
- * Endpoint: PATCH http://localhost:7001/user/v1/user/update
+ * Endpoint: PATCH {baseUrl}/user/v1/user/update
* Header: X-auth-token: {token}
* Content-Type: application/json
*/
@@ -17,21 +17,16 @@ object UserApiClient {
private val logger = LoggerFactory.getLogger(UserApiClient.getClass)
- // User Service Configuration
- private val BASE_URL = "http://172.132.44.221:7001"
- private val AUTH_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImlkIjozMDg3LCJuYW1lIjoiZmFyYWJpIFVwZGF0ZWQgdHdvIiwic2Vzc2lvbl9pZCI6MjMwMjgsIm9yZ2FuaXphdGlvbl9pZHMiOlsiNjciXSwib3JnYW5pemF0aW9uX2NvZGVzIjpbImJyYWNfZ2JsIl0sInRlbmFudF9jb2RlIjoiYnJhYyIsIm9yZ2FuaXphdGlvbnMiOlt7ImlkIjo2NywibmFtZSI6IkJSQUMgR0JMIG9yZyIsImNvZGUiOiJicmFjX2dibCIsImRlc2NyaXB0aW9uIjoiQlJBQyBHQkwgb3JnIiwic3RhdHVzIjoiQUNUSVZFIiwicmVsYXRlZF9vcmdzIjpudWxsLCJ0ZW5hbnRfY29kZSI6ImJyYWMiLCJtZXRhIjpudWxsLCJjcmVhdGVkX2J5IjpudWxsLCJ1cGRhdGVkX2J5IjoxLCJyb2xlcyI6W3siaWQiOjIxMywidGl0bGUiOiJzZXNzaW9uX21hbmFnZXIiLCJsYWJlbCI6IkxpbmthZ2UgQ2hhbXBpb24iLCJ1c2VyX3R5cGUiOjAsInN0YXR1cyI6IkFDVElWRSIsIm9yZ2FuaXphdGlvbl9pZCI6NjcsInZpc2liaWxpdHkiOiJQVUJMSUMiLCJ0ZW5hbnRfY29kZSI6ImJyYWMiLCJ0cmFuc2xhdGlvbnMiOm51bGx9XX1dfSwiaWF0IjoxNzY5NzUxNjcxLCJleHAiOjE3Njk4MzgwNzF9.wJpLStvXoU81vGQtIijs867BC7QJWO2F1w5F8W1X8-8"
-
- println(s"[UserApiClient] Initialized with BASE_URL: $BASE_URL")
- println(s"[UserApiClient] AUTH_TOKEN configured: ${if (AUTH_TOKEN.nonEmpty) "***" else "NOT SET"}")
-
/**
* Patch user profile data for a user
*
* @param id The user ID to update
* @param profileData JSON object containing profile data (e.g., {"profile": {"phone": "...", "gender": "..."}})
+ * @param baseUrl The base URL for the User Service API
+ * @param authToken The authentication token for the User Service API
* @return Success(true) if update successful, Failure(exception) otherwise
*/
- def patchProfile(id: String, profileData: Js.Obj): Try[Boolean] = {
+ def patchProfile(id: String, profileData: Js.Obj, baseUrl: String, authToken: String): Try[Boolean] = {
if (id == null || id.trim.isEmpty) {
val error = new IllegalArgumentException("id cannot be null or empty")
logger.error("[UserApiClient] id is null or empty", error)
@@ -40,7 +35,10 @@ object UserApiClient {
}
try {
- val url = s"$BASE_URL/user/v1/user/update"
+ val url = s"$baseUrl/user/v1/user/update"
+
+ println(s"[UserApiClient] Initialized with BASE_URL: $baseUrl")
+ println(s"[UserApiClient] AUTH_TOKEN configured: ${if (authToken.nonEmpty) "***" else "NOT SET"}")
// Merge id with profileData into a single payload
// The API expects the payload directly, so we'll include id and merge profile fields
@@ -69,7 +67,7 @@ object UserApiClient {
println(s"[UserApiClient] Request payload: $jsonPayload")
val headers = Map(
- "X-auth-token" -> AUTH_TOKEN,
+ "X-auth-token" -> authToken,
"Content-Type" -> "application/json"
)
@@ -109,10 +107,12 @@ object UserApiClient {
/**
* Test connection to user service (for debugging)
+ *
+ * @param baseUrl The base URL for the User Service API
*/
- def testConnection(): Try[Boolean] = {
+ def testConnection(baseUrl: String): Try[Boolean] = {
try {
- val url = s"$BASE_URL/api/users/health"
+ val url = s"$baseUrl/api/users/health"
println(s"[UserApiClient] Testing connection to: $url")
val response = requests.get(url)