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)