Skip to content

Commit b7a80ed

Browse files
codetuscanSartMa
andcommitted
Resolves #2322 Track supervisor id, decision and comment in anonymous profile
Adds supervisorId, supervisorDecision and supervisorComment to the anonymous profile so reports can be grouped by supervisor. supervisorId is already present in the packet operationsData and is read alongside officerId; it is now labelled on its own instead of only being part of assisted[]. The decision and comment are not in the packet at all - the supervisor takes the decision on the registration client and it reaches registration_list on sync. AnonymousProfileTagGenerator therefore reads them from there by workflowInstanceId, the same unique key SupervisorApprovalStatusTagGenerator uses, and adds them to the profile JSON it tags. A missing registration_list record leaves both fields null and the profile is still tagged. No schema change, no new API and no change to any existing method signature; existing profiles and dashboards are unaffected because the three fields are additive. Co-authored-by: Sarthak Maheshwari <65298686+SartMa@users.noreply.github.com> Signed-off-by: codetuscan <Sunhith.Reddy@iiitb.ac.in>
1 parent d89401e commit b7a80ed

5 files changed

Lines changed: 200 additions & 3 deletions

File tree

registration-processor/pre-processor/registration-processor-packet-classifier-stage/src/main/java/io/mosip/registration/processor/stages/packetclassifier/tagging/impl/AnonymousProfileTagGenerator.java

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.mosip.registration.processor.stages.packetclassifier.tagging.impl;
22

3+
import java.io.IOException;
34
import java.util.Collections;
45
import java.util.HashMap;
56
import java.util.List;
@@ -12,15 +13,21 @@
1213
import io.mosip.kernel.biometrics.entities.BiometricRecord;
1314
import io.mosip.kernel.core.exception.BaseCheckedException;
1415
import io.mosip.kernel.core.logger.spi.Logger;
16+
import io.mosip.registration.processor.core.anonymous.dto.AnonymousProfileDTO;
1517
import io.mosip.registration.processor.core.code.ModuleName;
1618
import io.mosip.registration.processor.core.constant.MappingJsonConstants;
1719
import io.mosip.registration.processor.core.constant.ProviderStageName;
1820
import io.mosip.registration.processor.core.logger.RegProcessorLogger;
21+
import io.mosip.registration.processor.core.util.JsonUtil;
1922
import io.mosip.registration.processor.packet.storage.utils.PriorityBasedPacketManagerService;
2023
import io.mosip.registration.processor.stages.packetclassifier.dto.FieldDTO;
2124
import io.mosip.registration.processor.stages.packetclassifier.tagging.TagGenerator;
2225
import io.mosip.registration.processor.status.code.RegistrationStatusCode;
26+
import io.mosip.registration.processor.status.dto.SyncRegistrationDto;
27+
import io.mosip.registration.processor.status.dto.SyncResponseDto;
28+
import io.mosip.registration.processor.status.entity.SyncRegistrationEntity;
2329
import io.mosip.registration.processor.status.service.AnonymousProfileService;
30+
import io.mosip.registration.processor.status.service.SyncRegistrationService;
2431

2532
/**
2633
* Builds the anonymous profile JSON and stores it as a packet tag so that
@@ -45,6 +52,9 @@ public class AnonymousProfileTagGenerator implements TagGenerator {
4552
@Autowired
4653
private PriorityBasedPacketManagerService priorityBasedPacketManagerService;
4754

55+
@Autowired
56+
private SyncRegistrationService<SyncResponseDto, SyncRegistrationDto> syncRegistrationService;
57+
4858
/**
4959
* No additional fields required — the processor already fetches the full
5060
* default-schema field set and passes it via idObjectFieldDTOMap.
@@ -92,7 +102,7 @@ public Map<String, String> generateTags(String workflowInstanceId, String regist
92102

93103
if (anonymousProfileJson != null && !anonymousProfileJson.isEmpty()) {
94104
Map<String, String> tags = new HashMap<>();
95-
tags.put(tagName, anonymousProfileJson);
105+
tags.put(tagName, addSupervisorDecision(anonymousProfileJson, workflowInstanceId, registrationId));
96106
return tags;
97107
}
98108
} catch (Exception e) {
@@ -102,4 +112,30 @@ public Map<String, String> generateTags(String workflowInstanceId, String regist
102112
}
103113
return Collections.emptyMap();
104114
}
115+
116+
/**
117+
* Adds the supervisor decision and comment to the profile JSON. Unlike every
118+
* other profile field these two are not in the packet - the supervisor takes
119+
* the decision on the registration client and it reaches registration_list on
120+
* sync, so they are read from there. Looked up by workflowInstanceId, a unique
121+
* key, the same way {@link SupervisorApprovalStatusTagGenerator} does.
122+
*
123+
* A missing sync record leaves both fields null and the profile is still
124+
* tagged - supervisor reporting must never cost us the profile itself.
125+
*/
126+
private String addSupervisorDecision(String anonymousProfileJson, String workflowInstanceId,
127+
String registrationId) throws IOException {
128+
SyncRegistrationEntity regEntity = syncRegistrationService.findByWorkflowInstanceId(workflowInstanceId);
129+
if (regEntity == null) {
130+
regProcLogger.warn(
131+
"AnonymousProfileTagGenerator: no registration_list record for {}; supervisor decision and comment left null",
132+
registrationId);
133+
return anonymousProfileJson;
134+
}
135+
AnonymousProfileDTO anonymousProfileDTO =
136+
JsonUtil.readValueWithUnknownProperties(anonymousProfileJson, AnonymousProfileDTO.class);
137+
anonymousProfileDTO.setSupervisorDecision(regEntity.getSupervisorStatus());
138+
anonymousProfileDTO.setSupervisorComment(regEntity.getSupervisorComment());
139+
return JsonUtil.objectMapperObjectToJson(anonymousProfileDTO);
140+
}
105141
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package io.mosip.registration.processor.stages.packetclassifier.tagging.impl;
2+
3+
import static org.junit.Assert.assertEquals;
4+
import static org.junit.Assert.assertNull;
5+
import static org.junit.Assert.assertTrue;
6+
import static org.mockito.ArgumentMatchers.any;
7+
import static org.mockito.ArgumentMatchers.anyString;
8+
9+
import java.util.HashMap;
10+
import java.util.List;
11+
import java.util.Map;
12+
13+
import org.junit.Before;
14+
import org.junit.Test;
15+
import org.junit.runner.RunWith;
16+
import org.mockito.InjectMocks;
17+
import org.mockito.Mock;
18+
import org.mockito.Mockito;
19+
import org.powermock.core.classloader.annotations.PowerMockIgnore;
20+
import org.powermock.modules.junit4.PowerMockRunner;
21+
import org.powermock.reflect.Whitebox;
22+
import org.springframework.cloud.context.config.annotation.RefreshScope;
23+
24+
import io.mosip.registration.processor.core.anonymous.dto.AnonymousProfileDTO;
25+
import io.mosip.registration.processor.core.util.JsonUtil;
26+
import io.mosip.registration.processor.packet.storage.utils.PriorityBasedPacketManagerService;
27+
import io.mosip.registration.processor.status.dto.SyncRegistrationDto;
28+
import io.mosip.registration.processor.status.dto.SyncResponseDto;
29+
import io.mosip.registration.processor.status.entity.SyncRegistrationEntity;
30+
import io.mosip.registration.processor.status.service.AnonymousProfileService;
31+
import io.mosip.registration.processor.status.service.SyncRegistrationService;
32+
33+
/**
34+
* The Class AnonymousProfileTagGeneratorTest.
35+
*/
36+
@RefreshScope
37+
@RunWith(PowerMockRunner.class)
38+
@PowerMockIgnore({ "javax.management.*", "javax.net.ssl.*", "com.sun.org.apache.xerces.*",
39+
"javax.xml.*", "org.xml.*" })
40+
public class AnonymousProfileTagGeneratorTest {
41+
42+
private static final String TAG_NAME = "anonymous";
43+
44+
private static final String WORKFLOW_INSTANCE_ID = "8e34c5d5-2ba1-4d69-9e60-31b0a1d1c1d0";
45+
46+
private static final String REGISTRATION_ID = "10001100010000120260824";
47+
48+
/**
49+
* The profile as AnonymousProfileServiceImpl builds it: supervisorId comes from
50+
* the packet operationsData, the decision and comment are not in the packet at
51+
* all and start out null.
52+
*/
53+
private static final String PROFILE_JSON = "{\"processName\":\"NEW\",\"status\":\"PROCESSING\","
54+
+ "\"assisted\":[\"110024\",\"SUP001\"],\"supervisorId\":\"SUP001\","
55+
+ "\"supervisorDecision\":null,\"supervisorComment\":null}";
56+
57+
@InjectMocks
58+
private AnonymousProfileTagGenerator anonymousProfileTagGenerator;
59+
60+
@Mock
61+
private AnonymousProfileService anonymousProfileService;
62+
63+
@Mock
64+
private PriorityBasedPacketManagerService priorityBasedPacketManagerService;
65+
66+
@Mock
67+
private SyncRegistrationService<SyncResponseDto, SyncRegistrationDto> syncRegistrationService;
68+
69+
@Before
70+
public void setup() throws Exception {
71+
Whitebox.setInternalState(anonymousProfileTagGenerator, "tagName", TAG_NAME);
72+
Mockito.when(anonymousProfileService.buildJsonStringFromPacketInfo(any(), any(), any(), any(), anyString(),
73+
anyString())).thenReturn(PROFILE_JSON);
74+
}
75+
76+
private Map<String, String> generateTags() throws Exception {
77+
return anonymousProfileTagGenerator.generateTags(WORKFLOW_INSTANCE_ID, REGISTRATION_ID, "NEW",
78+
new HashMap<>(), null, 0);
79+
}
80+
81+
private AnonymousProfileDTO taggedProfile(Map<String, String> tags) throws Exception {
82+
return JsonUtil.readValueWithUnknownProperties(tags.get(TAG_NAME), AnonymousProfileDTO.class);
83+
}
84+
85+
@Test
86+
public void supervisorDecisionAndCommentAreAddedFromRegistrationListTest() throws Exception {
87+
SyncRegistrationEntity syncRegistrationEntity = new SyncRegistrationEntity();
88+
syncRegistrationEntity.setSupervisorStatus("APPROVED");
89+
syncRegistrationEntity.setSupervisorComment("Verified by supervisor");
90+
Mockito.when(syncRegistrationService.findByWorkflowInstanceId(WORKFLOW_INSTANCE_ID))
91+
.thenReturn(syncRegistrationEntity);
92+
93+
AnonymousProfileDTO profile = taggedProfile(generateTags());
94+
95+
assertEquals("APPROVED", profile.getSupervisorDecision());
96+
assertEquals("Verified by supervisor", profile.getSupervisorComment());
97+
// the packet-sourced field must survive the enrichment round trip
98+
assertEquals("SUP001", profile.getSupervisorId());
99+
}
100+
101+
@Test
102+
public void rejectedDecisionIsCarriedThroughTest() throws Exception {
103+
SyncRegistrationEntity syncRegistrationEntity = new SyncRegistrationEntity();
104+
syncRegistrationEntity.setSupervisorStatus("REJECTED");
105+
syncRegistrationEntity.setSupervisorComment("Poor biometric quality");
106+
Mockito.when(syncRegistrationService.findByWorkflowInstanceId(anyString()))
107+
.thenReturn(syncRegistrationEntity);
108+
109+
AnonymousProfileDTO profile = taggedProfile(generateTags());
110+
111+
assertEquals("REJECTED", profile.getSupervisorDecision());
112+
assertEquals("Poor biometric quality", profile.getSupervisorComment());
113+
}
114+
115+
/** A packet with no registration_list record must still get its profile tagged. */
116+
@Test
117+
public void profileIsStillTaggedWhenSyncRecordIsMissingTest() throws Exception {
118+
Mockito.when(syncRegistrationService.findByWorkflowInstanceId(anyString())).thenReturn(null);
119+
120+
Map<String, String> tags = generateTags();
121+
122+
assertEquals(PROFILE_JSON, tags.get(TAG_NAME));
123+
assertNull(taggedProfile(tags).getSupervisorDecision());
124+
assertNull(taggedProfile(tags).getSupervisorComment());
125+
}
126+
127+
/**
128+
* The lookup is only attempted once a profile exists, and a build failure still
129+
* leaves classification unblocked with no tag - the workflow manager then falls
130+
* back to building the profile from the packet.
131+
*/
132+
@Test
133+
public void noTagAndNoLookupWhenProfileBuildFailsTest() throws Exception {
134+
Mockito.when(anonymousProfileService.buildJsonStringFromPacketInfo(any(), any(), any(), any(), anyString(),
135+
anyString())).thenThrow(new RuntimeException("profile build failed"));
136+
137+
assertTrue(generateTags().isEmpty());
138+
Mockito.verify(syncRegistrationService, Mockito.never()).findByWorkflowInstanceId(anyString());
139+
}
140+
141+
/** A registration_list read failure must not break packet classification. */
142+
@Test
143+
public void classificationContinuesWhenSupervisorLookupFailsTest() throws Exception {
144+
Mockito.when(syncRegistrationService.findByWorkflowInstanceId(anyString()))
145+
.thenThrow(new RuntimeException("registration_list unavailable"));
146+
147+
assertTrue(generateTags().isEmpty());
148+
}
149+
150+
@Test
151+
public void getRequiredIdObjectFieldNamesTest() throws Exception {
152+
List<String> result = anonymousProfileTagGenerator.getRequiredIdObjectFieldNames();
153+
assertTrue(result.isEmpty());
154+
}
155+
156+
}

registration-processor/registration-processor-core/src/main/java/io/mosip/registration/processor/core/anonymous/dto/AnonymousProfileDTO.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,7 @@ public class AnonymousProfileDTO {
2525
private List<String> assisted;
2626
private String enrollmentCenterId;
2727
private String status;
28+
private String supervisorId;
29+
private String supervisorDecision;
30+
private String supervisorComment;
2831
}

registration-processor/registration-processor-registration-status-service-impl/src/main/java/io/mosip/registration/processor/status/service/impl/AnonymousProfileServiceImpl.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,8 @@ public String buildJsonStringFromPacketInfo(BiometricRecord biometricRecord, Map
211211
assisted.add(supervisorId);
212212
}
213213
anonymousProfileDTO.setAssisted(assisted);
214+
// Same value as in assisted[], labelled separately so reports can group by supervisor.
215+
anonymousProfileDTO.setSupervisorId(supervisorId);
214216
getExceptionAndBiometricInfo(biometricRecord, anonymousProfileDTO);
215217

216218
regProcLogger.info("buildJsonStringFromPacketInfo method call ended");

registration-processor/registration-processor-registration-status-service-impl/src/test/java/io/mosip/registration/processor/status/service/AnonymousProfileServiceImplTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ public void saveAnonymousProfileDataAccessLayerExceptionTest() {
185185
@Test
186186
public void buildJsonStringFromPacketInfoTest() throws JSONException, IOException, BaseCheckedException {
187187

188-
String json = "{\"processName\":\"NEW\",\"processStage\":\"packetValidatorStage\",\"date\":\"2021-09-12T06:50:19.517872400Z\",\"startDateTime\":\"2021-09-12T06:50:19.517872400Z\",\"endDateTime\":\"2021-09-12T06:50:19.517872400Z\",\"yearOfBirth\":1998,\"gender\":\"Female\",\"location\":[\"Ben Mansour\",\"14022\"],\"preferredLanguages\":null,\"channel\":[\"phone\"],\"exceptions\":[],\"verified\":null,\"biometricInfo\":[{\"type\":\"FINGER\",\"subType\":\"Left RingFinger\",\"qualityScore\":80,\"attempts\":\"1\",\"digitalId\":\"9KgAwIBAgIBBT\"}],\"device\":null,\"documents\":[\"CIN\",\"RNC\"],\"assisted\":[\"110024\"],\"enrollmentCenterId\":\"1003\",\"status\":\"PROCESSED\"}";
188+
String json = "{\"processName\":\"NEW\",\"processStage\":\"packetValidatorStage\",\"date\":\"2021-09-12T06:50:19.517872400Z\",\"startDateTime\":\"2021-09-12T06:50:19.517872400Z\",\"endDateTime\":\"2021-09-12T06:50:19.517872400Z\",\"yearOfBirth\":1998,\"gender\":\"Female\",\"location\":[\"Ben Mansour\",\"14022\"],\"preferredLanguages\":null,\"channel\":[\"phone\"],\"exceptions\":[],\"verified\":null,\"biometricInfo\":[{\"type\":\"FINGER\",\"subType\":\"Left RingFinger\",\"qualityScore\":80,\"attempts\":\"1\",\"digitalId\":\"9KgAwIBAgIBBT\"}],\"device\":null,\"documents\":[\"CIN\",\"RNC\"],\"assisted\":[\"110024\"],\"enrollmentCenterId\":\"1003\",\"status\":\"PROCESSED\",\"supervisorId\":null,\"supervisorDecision\":null,\"supervisorComment\":null}";
189189
Document doc1 = new Document();
190190
doc1.setDocumentType("CIN");
191191
Document doc2 = new Document();
@@ -241,7 +241,7 @@ public void buildJsonStringFromPacketInfoVariousScenarioTest() throws JSONExcept
241241
metaInfoMap.put("operationsData",
242242
"[{\"label\" : \"supervisorId\",\"value\" : \"110024\"},{\"label\" : \"supervisorBiometricFileName\",\"value\" : \"null\"}]");
243243

244-
String json = "{\"processName\":\"NEW\",\"processStage\":\"packetValidatorStage\",\"date\":\"2021-09-12T06:50:19.517872400Z\",\"startDateTime\":\"2021-09-12T06:50:19.517872400Z\",\"endDateTime\":\"2021-09-12T06:50:19.517872400Z\",\"yearOfBirth\":1998,\"gender\":null,\"location\":[null,\"14022\"],\"preferredLanguages\":[\"English\"],\"channel\":[\"email\",\"phone\"],\"exceptions\":[{\"type\":\"FINGER\",\"subType\":\"Left RingFinger\"}],\"verified\":null,\"biometricInfo\":[],\"device\":null,\"documents\":[\"CIN\",\"RNC\"],\"assisted\":[\"110024\"],\"enrollmentCenterId\":\"1003\",\"status\":\"PROCESSED\"}";
244+
String json = "{\"processName\":\"NEW\",\"processStage\":\"packetValidatorStage\",\"date\":\"2021-09-12T06:50:19.517872400Z\",\"startDateTime\":\"2021-09-12T06:50:19.517872400Z\",\"endDateTime\":\"2021-09-12T06:50:19.517872400Z\",\"yearOfBirth\":1998,\"gender\":null,\"location\":[null,\"14022\"],\"preferredLanguages\":[\"English\"],\"channel\":[\"email\",\"phone\"],\"exceptions\":[{\"type\":\"FINGER\",\"subType\":\"Left RingFinger\"}],\"verified\":null,\"biometricInfo\":[],\"device\":null,\"documents\":[\"CIN\",\"RNC\"],\"assisted\":[\"110024\"],\"enrollmentCenterId\":\"1003\",\"status\":\"PROCESSED\",\"supervisorId\":\"110024\",\"supervisorDecision\":null,\"supervisorComment\":null}";
245245
Document doc1 = new Document();
246246
doc1.setDocumentType("CIN");
247247
Document doc2 = new Document();

0 commit comments

Comments
 (0)