Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #512: Invalid state when app gets closed during onboarding #515

Merged
merged 19 commits into from
Nov 11, 2022
Merged
Show file tree
Hide file tree
Changes from 16 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
Expand Up @@ -52,6 +52,9 @@ public class IdentityVerificationEntity implements Serializable {

public static final String ERROR_MAX_FAILED_ATTEMPTS_CLIENT_EVALUATION = "maxFailedAttemptsClientEvaluation";

public static final String DOCUMENT_VERIFICATION_FAILED = "documentVerificationFailed";
public static final String DOCUMENT_VERIFICATION_REJECTED = "documentVerificationRejected";

@Id
@GeneratedValue(generator = "uuid")
@GenericGenerator(name = "uuid", strategy = "uuid2")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,9 @@ public IdentityVerificationEntity updateIdentityVerification(IdentityVerificatio
*/
@Transactional
public IdentityVerificationEntity moveToPhaseAndStatus(final IdentityVerificationEntity identityVerification,
final IdentityVerificationPhase phase,
final IdentityVerificationStatus status,
final OwnerId ownerId) {
final IdentityVerificationPhase phase,
final IdentityVerificationStatus status,
final OwnerId ownerId) {

identityVerification.setPhase(phase);
identityVerification.setStatus(status);
Expand Down Expand Up @@ -212,16 +212,9 @@ public List<DocumentVerificationEntity> submitDocuments(DocumentSubmitRequest re

final IdentityVerificationPhase phase = idVerification.getPhase();
final IdentityVerificationStatus status = idVerification.getStatus();
if (phase == IdentityVerificationPhase.DOCUMENT_VERIFICATION && status == IdentityVerificationStatus.IN_PROGRESS) {
banterCZ marked this conversation as resolved.
Show resolved Hide resolved
moveToDocumentUpload(ownerId, idVerification, IdentityVerificationStatus.VERIFICATION_PENDING);
} else if (phase != DOCUMENT_UPLOAD) {
if (phase != IdentityVerificationPhase.DOCUMENT_UPLOAD || status != IdentityVerificationStatus.IN_PROGRESS) {
throw new DocumentSubmitException(
String.format("Not allowed submit of documents during not upload phase %s, %s", phase, ownerId));
} else if (IdentityVerificationStatus.VERIFICATION_PENDING.equals(status)) {
moveToDocumentUpload(ownerId, idVerification, IdentityVerificationStatus.IN_PROGRESS);
} else if (status != IdentityVerificationStatus.IN_PROGRESS) {
throw new DocumentSubmitException(
String.format("Not allowed submit of documents during not in progress status %s, %s", status, ownerId));
String.format("Not allowed submit of documents during not upload phase %s/%s, %s", phase, status, ownerId));
}

identityVerificationLimitService.checkDocumentUploadLimit(ownerId, idVerification);
Expand Down Expand Up @@ -328,24 +321,7 @@ public void checkVerificationResult(final OwnerId ownerId, final IdentityVerific
return;
}

if (!requiredDocumentTypesCheck.evaluate(idVerification.getDocumentVerifications(), idVerification.getId())) {
logger.debug("Not all required document types are present yet for identity verification ID: {}", idVerification.getId());
return;
}

moveToDocumentVerificationAndStatusByDocuments(idVerification, allDocVerifications, ownerId);

// Update process error score in case of a failed verification and check process error limits
if (idVerification.getStatus() == FAILED || idVerification.getStatus() == REJECTED) {
OnboardingProcessEntity process = processService.findProcess(idVerification.getProcessId());
if (idVerification.getStatus() == FAILED) {
processLimitService.incrementErrorScore(process, OnboardingProcessError.ERROR_DOCUMENT_VERIFICATION_FAILED, ownerId);
}
if (idVerification.getStatus() == REJECTED) {
processLimitService.incrementErrorScore(process, OnboardingProcessError.ERROR_DOCUMENT_VERIFICATION_REJECTED, ownerId);
}
processLimitService.checkOnboardingProcessErrorLimits(process);
}
}

/**
Expand All @@ -361,7 +337,7 @@ public void processDocumentVerificationResult(final OwnerId ownerId, final Ident
logger.debug("Final validation passed, {}", ownerId);
moveToPhaseAndStatus(idVerification, IdentityVerificationPhase.COMPLETED, ACCEPTED, ownerId);
} else {
logger.warn("Final validation did not passed, marking identity verification as failed due to '{}', {}", result.getErrorDetail(), ownerId);
logger.warn("Final validation did not pass, marking identity verification as failed due to '{}', {}", result.getErrorDetail(), ownerId);
idVerification.setErrorDetail(result.getErrorDetail());
idVerification.setTimestampFailed(ownerId.getTimestamp());
idVerification.setErrorOrigin(ErrorOrigin.FINAL_VALIDATION);
Expand All @@ -376,40 +352,96 @@ public void processDocumentVerificationResult(final OwnerId ownerId, final Ident
* Move identity verification to {@code DOCUMENT_VERIFICATION} phase and status based on the given document verifications.
*
* @param idVerification Identity verification entity.
* @param docVerifications Document verifications to determine identity verification status.
* @param docVerificationsToProcess Document verifications to determine identity verification status.
*/
private void moveToDocumentVerificationAndStatusByDocuments(
final IdentityVerificationEntity idVerification,
final List<DocumentVerificationEntity> docVerifications,
final List<DocumentVerificationEntity> docVerificationsToProcess,
final OwnerId ownerId) {

final IdentityVerificationPhase phase = IdentityVerificationPhase.DOCUMENT_VERIFICATION;
final Date now = ownerId.getTimestamp();
if (docVerifications.stream()
final String identityVerificationId = idVerification.getId();
// docVerificationsToProcess have been modified, so idVerification.getDocumentVerifications() must be reloaded to reflect these modifications
banterCZ marked this conversation as resolved.
Show resolved Hide resolved
final List<DocumentVerificationEntity> allDocumentVerifications = documentVerificationRepository.findAllUsedForVerification(idVerification);

final boolean allRequiredDocumentsChecked = requiredDocumentTypesCheck.evaluate(allDocumentVerifications, identityVerificationId);
if (!allRequiredDocumentsChecked) {
logger.debug("Not all required document types are present yet for identity verification ID: {}", identityVerificationId);
} else {
logger.debug("All required document types are present for identity verification ID: {}", identityVerificationId);
}

if (docVerificationsToProcess.stream()
.map(DocumentVerificationEntity::getStatus)
.allMatch(it -> it == DocumentStatus.ACCEPTED)) {
// The timestampFinished parameter is not set yet, there may be other steps ahead
moveToPhaseAndStatus(idVerification, phase, ACCEPTED, ownerId);
if (allRequiredDocumentsChecked) {
// Move to DOCUMENT_VERIFICATION / ACCEPTED only in case all documents were checked
moveToPhaseAndStatus(idVerification, IdentityVerificationPhase.DOCUMENT_VERIFICATION, ACCEPTED, ownerId);
} else {
// Identity verification status is changed to DOCUMENT_UPLOAD / IN_PROGRESS to allow submission of additional documents
moveToDocumentUpload(ownerId, idVerification, IN_PROGRESS);
}
} else {
// Identity verification status is changed to DOCUMENT_UPLOAD / IN_PROGRESS to allow re-submission of failed documents
moveToDocumentUpload(ownerId, idVerification, IN_PROGRESS);
handleDocumentStatus(docVerificationsToProcess, idVerification, DocumentStatus.FAILED, ownerId);
handleDocumentStatus(docVerificationsToProcess, idVerification, DocumentStatus.REJECTED, ownerId);
}
}

private void handleDocumentStatus(
final List<DocumentVerificationEntity> docVerifications,
final IdentityVerificationEntity idVerification,
final DocumentStatus status,
final OwnerId ownerId) {

docVerifications.stream()
.filter(docVerification -> docVerification.getStatus() == status)
.findAny()
.ifPresent(docVerification -> {
logger.debug("At least one document is {}, ID: {}, {}", status, docVerification.getId(), ownerId);
idVerification.setErrorDetail(fetchErrorDetail(docVerification.getStatus()));
idVerification.setErrorOrigin(ErrorOrigin.DOCUMENT_VERIFICATION);
handleLimitsForRejectOrFail(idVerification, status, ownerId);
});
}


private static String fetchErrorDetail(final DocumentStatus status) {
if (status == DocumentStatus.REJECTED) {
return IdentityVerificationEntity.DOCUMENT_VERIFICATION_REJECTED;
} else if (status == DocumentStatus.FAILED) {
return IdentityVerificationEntity.DOCUMENT_VERIFICATION_FAILED;
} else {
docVerifications.stream()
.filter(docVerification -> docVerification.getStatus() == DocumentStatus.FAILED)
.findAny()
.ifPresent(failed -> {
idVerification.setErrorDetail(failed.getErrorDetail());
idVerification.setTimestampFailed(now);
idVerification.setErrorOrigin(ErrorOrigin.DOCUMENT_VERIFICATION);
moveToPhaseAndStatus(idVerification, phase, FAILED, ownerId);
});

docVerifications.stream()
.filter(docVerification -> docVerification.getStatus() == DocumentStatus.REJECTED)
.findAny()
.ifPresent(failed -> {
idVerification.setErrorDetail(failed.getRejectReason());
idVerification.setErrorOrigin(ErrorOrigin.DOCUMENT_VERIFICATION);
idVerification.setTimestampFinished(now);
moveToPhaseAndStatus(idVerification, phase, REJECTED, ownerId);
});
return "";
}
}

/**
* Update process error score in case of a rejected or a failed verification and check process error limits.
*
* @param idVerification Identity verification entity.
* @param status Identity verification status.
* @param ownerId Owner identifier.
*/
private void handleLimitsForRejectOrFail(IdentityVerificationEntity idVerification, DocumentStatus status, OwnerId ownerId) {
if (status == DocumentStatus.FAILED || status == DocumentStatus.REJECTED) {
final OnboardingProcessEntity process;
try {
process = processService.findProcess(idVerification.getProcessId());
} catch (OnboardingProcessException e) {
logger.trace("Onboarding process not found, {}", ownerId, e);
logger.warn("Onboarding process not found, {}, {}", e.getMessage(), ownerId);
return;
}

if (status == DocumentStatus.FAILED) {
processLimitService.incrementErrorScore(process, OnboardingProcessError.ERROR_DOCUMENT_VERIFICATION_FAILED, ownerId);
}
if (status == DocumentStatus.REJECTED) {
processLimitService.incrementErrorScore(process, OnboardingProcessError.ERROR_DOCUMENT_VERIFICATION_REJECTED, ownerId);
}
processLimitService.checkOnboardingProcessErrorLimits(process);
}
}

Expand Down Expand Up @@ -558,7 +590,7 @@ public Stream<IdentityVerificationEntity> streamAllIdentityVerificationsToChange
}

private void moveToDocumentUpload(final OwnerId ownerId, final IdentityVerificationEntity idVerification, final IdentityVerificationStatus status) {
logger.debug("New documents submitted, moving to DOCUMENT_UPLOAD; {}", ownerId);
logger.debug("Moving phase to DOCUMENT_UPLOAD, {}", ownerId);
moveToPhaseAndStatus(idVerification, DOCUMENT_UPLOAD, status, ownerId);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
package com.wultra.app.onboardingserver.statemachine.action.verification;

import com.wultra.app.enrollmentserver.model.enumeration.DocumentStatus;
import com.wultra.app.enrollmentserver.model.integration.OwnerId;
import com.wultra.app.onboardingserver.common.database.DocumentVerificationRepository;
import com.wultra.app.onboardingserver.common.database.entity.DocumentVerificationEntity;
import com.wultra.app.onboardingserver.common.database.entity.IdentityVerificationEntity;
import com.wultra.app.onboardingserver.statemachine.consts.EventHeaderName;
import com.wultra.app.onboardingserver.statemachine.consts.ExtendedStateVariable;
import com.wultra.app.onboardingserver.statemachine.enums.OnboardingEvent;
import com.wultra.app.onboardingserver.statemachine.enums.OnboardingState;
Expand All @@ -33,7 +35,7 @@
import java.util.List;

/**
* Guard that all documents for verification of the given identity verification are in status {@code VERIFICATION_PENDING} or {@code ACCEPTED}.
* Guard for presence of document verification in status {@code VERIFICATION_PENDING}.
*
* @author Lubos Racansky, lubos.racansky@wultra.com
*/
Expand All @@ -51,24 +53,25 @@ public DocumentsVerificationPendingGuard(final DocumentVerificationRepository do
@Override
@Transactional(readOnly = true)
public boolean evaluate(final StateContext<OnboardingState, OnboardingEvent> context) {
final OwnerId ownerId = (OwnerId) context.getMessageHeader(EventHeaderName.OWNER_ID);
final IdentityVerificationEntity identityVerification = context.getExtendedState().get(ExtendedStateVariable.IDENTITY_VERIFICATION, IdentityVerificationEntity.class);

final List<DocumentVerificationEntity> documentVerifications = documentVerificationRepository.findAllUsedForVerification(identityVerification);
if (documentVerifications.isEmpty()) {
logger.debug("No document uploaded yet for {}", identityVerification);
logger.debug("No document uploaded yet for {}, {}", identityVerification, ownerId);
return false;
}

final boolean allDocumentsPendingVerification = documentVerifications.stream()
final boolean pendingVerificationDocumentPresent = documentVerifications.stream()
.map(DocumentVerificationEntity::getStatus)
.allMatch(it -> it == DocumentStatus.VERIFICATION_PENDING || it == DocumentStatus.ACCEPTED);
.anyMatch(it -> it == DocumentStatus.VERIFICATION_PENDING);

if (allDocumentsPendingVerification) {
logger.info("All documents for verification of {} are pending verification or accepted", identityVerification);
if (pendingVerificationDocumentPresent) {
logger.info("Pending verification document present for {}, {}", identityVerification, ownerId);
} else {
logger.debug("Not all documents for verification of {} are pending verification or accepted", identityVerification);
logger.debug("No pending verification document present for {}, {}", identityVerification, ownerId);
}

return allDocumentsPendingVerification;
return pendingVerificationDocumentPresent;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ void testPendingVerificationAndAcceptedAndFailed() {
createDocumentVerification(DocumentStatus.FAILED)));

final boolean result = tested.evaluate(context);
assertFalse(result);
assertTrue(result);
}

private DocumentVerificationEntity createDocumentVerification(final DocumentStatus status) {
Expand Down