Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.mosip.registration.processor.stages.packetclassifier.tagging.impl;

import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
Expand All @@ -12,15 +13,21 @@
import io.mosip.kernel.biometrics.entities.BiometricRecord;
import io.mosip.kernel.core.exception.BaseCheckedException;
import io.mosip.kernel.core.logger.spi.Logger;
import io.mosip.registration.processor.core.anonymous.dto.AnonymousProfileDTO;
import io.mosip.registration.processor.core.code.ModuleName;
import io.mosip.registration.processor.core.constant.MappingJsonConstants;
import io.mosip.registration.processor.core.constant.ProviderStageName;
import io.mosip.registration.processor.core.logger.RegProcessorLogger;
import io.mosip.registration.processor.core.util.JsonUtil;
import io.mosip.registration.processor.packet.storage.utils.PriorityBasedPacketManagerService;
import io.mosip.registration.processor.stages.packetclassifier.dto.FieldDTO;
import io.mosip.registration.processor.stages.packetclassifier.tagging.TagGenerator;
import io.mosip.registration.processor.status.code.RegistrationStatusCode;
import io.mosip.registration.processor.status.dto.SyncRegistrationDto;
import io.mosip.registration.processor.status.dto.SyncResponseDto;
import io.mosip.registration.processor.status.entity.SyncRegistrationEntity;
import io.mosip.registration.processor.status.service.AnonymousProfileService;
import io.mosip.registration.processor.status.service.SyncRegistrationService;

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

@Autowired
private SyncRegistrationService<SyncResponseDto, SyncRegistrationDto> syncRegistrationService;

/**
* No additional fields required — the processor already fetches the full
* default-schema field set and passes it via idObjectFieldDTOMap.
Expand Down Expand Up @@ -91,8 +101,19 @@ public Map<String, String> generateTags(String workflowInstanceId, String regist
ModuleName.PACKET_CLASSIFIER.toString());

if (anonymousProfileJson != null && !anonymousProfileJson.isEmpty()) {
// Supervisor decision is fetched here; failure is non-fatal and costs
// only those two fields, never the profile itself
String profileJson = anonymousProfileJson;
try {
profileJson = addSupervisorDecision(anonymousProfileJson, workflowInstanceId, registrationId);
} catch (Exception e) {
regProcLogger.warn(
"AnonymousProfileTagGenerator: supervisor decision fetch failed for {}; profile tagged without it. Error: {}",
registrationId, e.getMessage());
}

Map<String, String> tags = new HashMap<>();
tags.put(tagName, anonymousProfileJson);
tags.put(tagName, profileJson);
return tags;
}
} catch (Exception e) {
Expand All @@ -102,4 +123,33 @@ public Map<String, String> generateTags(String workflowInstanceId, String regist
}
return Collections.emptyMap();
}

/**
* Adds the supervisor decision and comment to the profile JSON. Unlike every
* other profile field these two are not in the packet - the supervisor takes
* the decision on the registration client and it reaches registration_list on
* sync, so they are read from there. Looked up by workflowInstanceId, a unique
* key, the same way {@link SupervisorApprovalStatusTagGenerator} does.
*
* A missing sync record leaves both fields null and the profile is still
* tagged. The record is usually absent at classification time because the
* supervisor decision has not synced yet, so that is logged at debug and the
* caller treats any failure here as non-fatal - supervisor reporting must
* never cost us the profile itself.
*/
private String addSupervisorDecision(String anonymousProfileJson, String workflowInstanceId,
String registrationId) throws IOException {
SyncRegistrationEntity regEntity = syncRegistrationService.findByWorkflowInstanceId(workflowInstanceId);
if (regEntity == null) {
regProcLogger.debug(
"AnonymousProfileTagGenerator: no registration_list record for {}; supervisor decision and comment left null",
registrationId);
return anonymousProfileJson;
}
AnonymousProfileDTO anonymousProfileDTO =
JsonUtil.readValueWithUnknownProperties(anonymousProfileJson, AnonymousProfileDTO.class);
anonymousProfileDTO.setSupervisorDecision(regEntity.getSupervisorStatus());
anonymousProfileDTO.setSupervisorComment(regEntity.getSupervisorComment());
return JsonUtil.objectMapperObjectToJson(anonymousProfileDTO);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package io.mosip.registration.processor.stages.packetclassifier.tagging.impl;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.modules.junit4.PowerMockRunner;
import org.powermock.reflect.Whitebox;
import org.springframework.cloud.context.config.annotation.RefreshScope;

import io.mosip.registration.processor.core.anonymous.dto.AnonymousProfileDTO;
import io.mosip.registration.processor.core.util.JsonUtil;
import io.mosip.registration.processor.packet.storage.utils.PriorityBasedPacketManagerService;
import io.mosip.registration.processor.status.dto.SyncRegistrationDto;
import io.mosip.registration.processor.status.dto.SyncResponseDto;
import io.mosip.registration.processor.status.entity.SyncRegistrationEntity;
import io.mosip.registration.processor.status.service.AnonymousProfileService;
import io.mosip.registration.processor.status.service.SyncRegistrationService;

/**
* The Class AnonymousProfileTagGeneratorTest.
*/
@RefreshScope
@RunWith(PowerMockRunner.class)
@PowerMockIgnore({ "javax.management.*", "javax.net.ssl.*", "com.sun.org.apache.xerces.*",
"javax.xml.*", "org.xml.*" })
public class AnonymousProfileTagGeneratorTest {

private static final String TAG_NAME = "anonymous";

private static final String WORKFLOW_INSTANCE_ID = "8e34c5d5-2ba1-4d69-9e60-31b0a1d1c1d0";

private static final String REGISTRATION_ID = "10001100010000120260824";

/**
* The profile as AnonymousProfileServiceImpl builds it: supervisorId comes from
* the packet operationsData, the decision and comment are not in the packet at
* all and start out null.
*/
private static final String PROFILE_JSON = "{\"processName\":\"NEW\",\"status\":\"PROCESSING\","
+ "\"assisted\":[\"110024\",\"SUP001\"],\"supervisorId\":\"SUP001\","
+ "\"supervisorDecision\":null,\"supervisorComment\":null}";

@InjectMocks
private AnonymousProfileTagGenerator anonymousProfileTagGenerator;

@Mock
private AnonymousProfileService anonymousProfileService;

@Mock
private PriorityBasedPacketManagerService priorityBasedPacketManagerService;

@Mock
private SyncRegistrationService<SyncResponseDto, SyncRegistrationDto> syncRegistrationService;

@Before
public void setup() throws Exception {
Whitebox.setInternalState(anonymousProfileTagGenerator, "tagName", TAG_NAME);
Mockito.when(anonymousProfileService.buildJsonStringFromPacketInfo(any(), any(), any(), any(), anyString(),
anyString())).thenReturn(PROFILE_JSON);
}

private Map<String, String> generateTags() throws Exception {
return anonymousProfileTagGenerator.generateTags(WORKFLOW_INSTANCE_ID, REGISTRATION_ID, "NEW",
new HashMap<>(), null, 0);
}

private AnonymousProfileDTO taggedProfile(Map<String, String> tags) throws Exception {
return JsonUtil.readValueWithUnknownProperties(tags.get(TAG_NAME), AnonymousProfileDTO.class);
}

@Test
public void supervisorDecisionAndCommentAreAddedFromRegistrationListTest() throws Exception {
SyncRegistrationEntity syncRegistrationEntity = new SyncRegistrationEntity();
syncRegistrationEntity.setSupervisorStatus("APPROVED");
syncRegistrationEntity.setSupervisorComment("Verified by supervisor");
Mockito.when(syncRegistrationService.findByWorkflowInstanceId(WORKFLOW_INSTANCE_ID))
.thenReturn(syncRegistrationEntity);

AnonymousProfileDTO profile = taggedProfile(generateTags());

assertEquals("APPROVED", profile.getSupervisorDecision());
assertEquals("Verified by supervisor", profile.getSupervisorComment());
// the packet-sourced field must survive the enrichment round trip
assertEquals("SUP001", profile.getSupervisorId());
}

@Test
public void rejectedDecisionIsCarriedThroughTest() throws Exception {
SyncRegistrationEntity syncRegistrationEntity = new SyncRegistrationEntity();
syncRegistrationEntity.setSupervisorStatus("REJECTED");
syncRegistrationEntity.setSupervisorComment("Poor biometric quality");
Mockito.when(syncRegistrationService.findByWorkflowInstanceId(anyString()))
.thenReturn(syncRegistrationEntity);

AnonymousProfileDTO profile = taggedProfile(generateTags());

assertEquals("REJECTED", profile.getSupervisorDecision());
assertEquals("Poor biometric quality", profile.getSupervisorComment());
}

/** A packet with no registration_list record must still get its profile tagged. */
@Test
public void profileIsStillTaggedWhenSyncRecordIsMissingTest() throws Exception {
Mockito.when(syncRegistrationService.findByWorkflowInstanceId(anyString())).thenReturn(null);

Map<String, String> tags = generateTags();

assertEquals(PROFILE_JSON, tags.get(TAG_NAME));
assertNull(taggedProfile(tags).getSupervisorDecision());
assertNull(taggedProfile(tags).getSupervisorComment());
}

/**
* The lookup is only attempted once a profile exists, and a build failure still
* leaves classification unblocked with no tag - the workflow manager then falls
* back to building the profile from the packet.
*/
@Test
public void noTagAndNoLookupWhenProfileBuildFailsTest() throws Exception {
Mockito.when(anonymousProfileService.buildJsonStringFromPacketInfo(any(), any(), any(), any(), anyString(),
anyString())).thenThrow(new RuntimeException("profile build failed"));

assertTrue(generateTags().isEmpty());
Mockito.verify(syncRegistrationService, Mockito.never()).findByWorkflowInstanceId(anyString());
}

/**
* A registration_list read failure costs the two supervisor fields, not the
* profile - the tag is still written with the JSON as it was built from the
* packet, exactly as a biometrics fetch failure only drops the biometrics.
*/
@Test
public void profileIsStillTaggedWhenSupervisorLookupFailsTest() throws Exception {
Mockito.when(syncRegistrationService.findByWorkflowInstanceId(anyString()))
.thenThrow(new RuntimeException("registration_list unavailable"));

Map<String, String> tags = generateTags();

assertEquals(PROFILE_JSON, tags.get(TAG_NAME));
assertEquals("SUP001", taggedProfile(tags).getSupervisorId());
assertNull(taggedProfile(tags).getSupervisorDecision());
assertNull(taggedProfile(tags).getSupervisorComment());
}

@Test
public void getRequiredIdObjectFieldNamesTest() throws Exception {
List<String> result = anonymousProfileTagGenerator.getRequiredIdObjectFieldNames();
assertTrue(result.isEmpty());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,7 @@ public class AnonymousProfileDTO {
private List<String> assisted;
private String enrollmentCenterId;
private String status;
private String supervisorId;
private String supervisorDecision;
private String supervisorComment;
}
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,8 @@ public String buildJsonStringFromPacketInfo(BiometricRecord biometricRecord, Map
assisted.add(supervisorId);
}
anonymousProfileDTO.setAssisted(assisted);
// Same value as in assisted[], labelled separately so reports can group by supervisor.
anonymousProfileDTO.setSupervisorId(supervisorId);
getExceptionAndBiometricInfo(biometricRecord, anonymousProfileDTO);

regProcLogger.info("buildJsonStringFromPacketInfo method call ended");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ public void saveAnonymousProfileDataAccessLayerExceptionTest() {
@Test
public void buildJsonStringFromPacketInfoTest() throws JSONException, IOException, BaseCheckedException {

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\"}";
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}";
Document doc1 = new Document();
doc1.setDocumentType("CIN");
Document doc2 = new Document();
Expand Down Expand Up @@ -241,7 +241,7 @@ public void buildJsonStringFromPacketInfoVariousScenarioTest() throws JSONExcept
metaInfoMap.put("operationsData",
"[{\"label\" : \"supervisorId\",\"value\" : \"110024\"},{\"label\" : \"supervisorBiometricFileName\",\"value\" : \"null\"}]");

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\"}";
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}";
Document doc1 = new Document();
doc1.setDocumentType("CIN");
Document doc2 = new Document();
Expand Down