diff --git a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsApiService.java b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsApiService.java
index 8d99e710e..916c08fb7 100644
--- a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsApiService.java
+++ b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsApiService.java
@@ -18,7 +18,13 @@
package com.wultra.app.onboardingserver.provider.innovatrics;
import com.wultra.app.enrollmentserver.model.integration.OwnerId;
+import com.wultra.app.onboardingserver.api.errorhandling.DocumentVerificationException;
import com.wultra.app.onboardingserver.common.errorhandling.RemoteCommunicationException;
+import com.wultra.app.onboardingserver.provider.innovatrics.model.api.CustomerInspectResponse;
+import com.wultra.app.onboardingserver.provider.innovatrics.model.api.EvaluateCustomerLivenessRequest;
+import com.wultra.app.onboardingserver.provider.innovatrics.model.api.EvaluateCustomerLivenessResponse;
+import com.wultra.app.enrollmentserver.model.enumeration.CardSide;
+import com.wultra.app.enrollmentserver.model.enumeration.DocumentType;
import com.wultra.app.onboardingserver.provider.innovatrics.model.api.*;
import com.wultra.core.rest.client.base.RestClient;
import com.wultra.core.rest.client.base.RestClientException;
@@ -29,11 +35,15 @@
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
+import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
+import java.util.List;
+import java.util.Optional;
+
/**
* Implementation of the REST service toInnovatrics.
*
@@ -41,6 +51,7 @@
* Both providers, document verifier and presence check, must be configured to {@code innovatrics}.
*
* @author Lubos Racansky, lubos.racansky@wultra.com
+ * @author Jan Pesek, jan.pesek@wultra.com
*/
@ConditionalOnExpression("""
'${enrollment-server-onboarding.presence-check.provider}' == 'innovatrics' and '${enrollment-server-onboarding.document-verification.provider}' == 'innovatrics'
@@ -49,8 +60,6 @@
@Slf4j
class InnovatricsApiService {
- private static final MultiValueMap EMPTY_ADDITIONAL_HEADERS = new LinkedMultiValueMap<>();
-
private static final MultiValueMap EMPTY_QUERY_PARAMS = new LinkedMultiValueMap<>();
/**
@@ -58,14 +67,21 @@ class InnovatricsApiService {
*/
private final RestClient restClient;
+ /**
+ * Configuration properties.
+ */
+ private final InnovatricsConfigProps configProps;
+
/**
* Service constructor.
*
* @param restClient REST template for Innovatrics calls.
*/
@Autowired
- public InnovatricsApiService(@Qualifier("restClientInnovatrics") final RestClient restClient) {
+ public InnovatricsApiService(@Qualifier("restClientInnovatrics") final RestClient restClient,
+ InnovatricsConfigProps configProps) {
this.restClient = restClient;
+ this.configProps = configProps;
}
public EvaluateCustomerLivenessResponse evaluateLiveness(final String customerId, final OwnerId ownerId) throws RemoteCommunicationException {
@@ -77,7 +93,7 @@ public EvaluateCustomerLivenessResponse evaluateLiveness(final String customerId
try {
logger.info("Calling liveness/evaluation, {}", ownerId);
logger.debug("Calling {}, {}", apiPath, request);
- final ResponseEntity response = restClient.post(apiPath, request, EMPTY_QUERY_PARAMS, EMPTY_ADDITIONAL_HEADERS, new ParameterizedTypeReference<>() {});
+ final ResponseEntity response = restClient.post(apiPath, request, new ParameterizedTypeReference<>() {});
logger.info("Got {} for liveness/evaluation, {}", response.getStatusCode(), ownerId);
logger.debug("{} response status code: {}", apiPath, response.getStatusCode());
logger.trace("{} response: {}", apiPath, response);
@@ -96,7 +112,7 @@ public CustomerInspectResponse inspectCustomer(final String customerId, final Ow
try {
logger.info("Calling /inspect, {}", ownerId);
logger.debug("Calling {}", apiPath);
- final ResponseEntity response = restClient.post(apiPath, null, EMPTY_QUERY_PARAMS, EMPTY_ADDITIONAL_HEADERS, new ParameterizedTypeReference<>() {});
+ final ResponseEntity response = restClient.post(apiPath, null, new ParameterizedTypeReference<>() {});
logger.info("Got {} for /inspect, {}", response.getStatusCode(), ownerId);
logger.debug("{} response status code: {}", apiPath, response.getStatusCode());
logger.trace("{} response: {}", apiPath, response);
@@ -206,4 +222,255 @@ public CreateSelfieResponse createSelfie(final String customerId, final String l
}
}
+ // TODO remove - temporal test call
+// @PostConstruct
+// public void testCall() throws RestClientException {
+// logger.info("Trying a test call");
+// final ResponseEntity response = restClient.get("/api/v1/metadata", STRING_TYPE_REFERENCE);
+// logger.info("Result of test call: {}", response.getBody());
+// }
+
+ /**
+ * Create a new customer resource.
+ * @param ownerId owner identification.
+ * @return optional of CreateCustomerResponse with a customerId.
+ * @throws RemoteCommunicationException in case of 4xx or 5xx response status code.
+ */
+ public CreateCustomerResponse createCustomer(final OwnerId ownerId) throws RemoteCommunicationException {
+ final String apiPath = "/api/v1/customers";
+
+ try {
+ logger.info("Creating customer, {}", ownerId);
+ logger.debug("Calling {}", apiPath);
+ final ResponseEntity response = restClient.post(apiPath, null, new ParameterizedTypeReference<>() {});
+ logger.info("Got {} for creating customer, {}", response.getStatusCode(), ownerId);
+ logger.debug("{} response status code: {}", apiPath, response.getStatusCode());
+ logger.trace("{} response: {}", apiPath, response);
+ return response.getBody();
+ } catch (RestClientException e) {
+ throw new RemoteCommunicationException("REST API call failed when creating a new customer resource, statusCode=%s, responseBody='%s'".formatted(e.getStatusCode(), e.getResponse()), e);
+ } catch (Exception e) {
+ throw new RemoteCommunicationException("Unexpected error when creating a new customer resource", e);
+ }
+ }
+
+ /**
+ * Create a new document resource assigned to a customer. This resource is used for documents only, not for selfies.
+ * @param customerId id of the customer to assign the resource to.
+ * @param documentType type of document that will be uploaded later.
+ * @param ownerId owner identification.
+ * @return optional of CreateDocumentResponse. Does not contain important details.
+ * @throws RemoteCommunicationException in case of 4xx or 5xx response status code.
+ */
+ public CreateDocumentResponse createDocument(final String customerId, final DocumentType documentType, final OwnerId ownerId) throws RemoteCommunicationException, DocumentVerificationException {
+ final String apiPath = "/api/v1/customers/%s/document".formatted(customerId);
+
+ final DocumentClassificationAdvice classificationAdvice = new DocumentClassificationAdvice();
+ classificationAdvice.setTypes(List.of(convertType(documentType)));
+ classificationAdvice.setCountries(configProps.getDocumentVerificationConfiguration().getDocumentCountries());
+ final DocumentAdvice advice = new DocumentAdvice();
+ advice.setClassification(classificationAdvice);
+ final CreateDocumentRequest request = new CreateDocumentRequest();
+ request.setAdvice(advice);
+
+ try {
+ logger.info("Creating new document of type {} for customer {}, {}", documentType, customerId, ownerId);
+ logger.debug("Calling {}, {}", apiPath, request);
+ final ResponseEntity response = restClient.put(apiPath, request, new ParameterizedTypeReference<>() {});
+ logger.info("Got {} for creating document, {}", response.getStatusCode(), ownerId);
+ logger.debug("{} response status code: {}", apiPath, response.getStatusCode());
+ logger.trace("{} response: {}", apiPath, response);
+ return response.getBody();
+ } catch (RestClientException e) {
+ throw new RemoteCommunicationException("REST API call failed when creating a new document resource for customerId=%s, statusCode=%s, responseBody='%s'".formatted(customerId, e.getStatusCode(), e.getResponse()), e);
+ } catch (Exception e) {
+ throw new RemoteCommunicationException("Unexpected error when creating a new document resource for customerId=%s".formatted(customerId), e);
+ }
+ }
+
+ /**
+ * Provide photo of a document page. A document resource must be already assigned to the customer.
+ * @param customerId id of the customer to whom the document should be provided.
+ * @param side specifies side of the document.
+ * @param imageBytes image of the page encoded in base64.
+ * @param ownerId owner identification.
+ * @return optional of CreateDocumentPageResponse with details extracted from the page.
+ * @throws RemoteCommunicationException in case of 4xx or 5xx response status code.
+ */
+ public CreateDocumentPageResponse provideDocumentPage(final String customerId, final CardSide side, final byte[] imageBytes, final OwnerId ownerId) throws RemoteCommunicationException {
+ final String apiPath = "/api/v1/customers/%s/document/pages".formatted(customerId);
+
+ final DocumentPageClassificationAdvice classificationAdvice = new DocumentPageClassificationAdvice();
+ classificationAdvice.setPageTypes(List.of(convertSide(side)));
+ final DocumentPageAdvice advice = new DocumentPageAdvice();
+ advice.setClassification(classificationAdvice);
+
+ final Image image = new Image();
+ image.setData(imageBytes);
+
+ final CreateDocumentPageRequest request = new CreateDocumentPageRequest();
+ request.setAdvice(advice);
+ request.setImage(image);
+
+ try {
+ logger.info("Providing {} side document page for customer {}, {}", convertSide(side), customerId, ownerId);
+ logger.debug("Calling {}, {}", apiPath, request);
+ final ResponseEntity response = restClient.put(apiPath, request, new ParameterizedTypeReference<>() {});
+ logger.info("Got {} for providing document page, {}", response.getStatusCode(), ownerId);
+ logger.debug("{} response status code: {}", apiPath, response.getStatusCode());
+ logger.trace("{} response: {}", apiPath, response);
+ return response.getBody();
+ } catch (RestClientException e) {
+ throw new RemoteCommunicationException("REST API call failed when providing a document page for customerId=%s, statusCode=%s, responseBody='%s'".formatted(customerId, e.getStatusCode(), e.getResponse()), e);
+ } catch (Exception e) {
+ throw new RemoteCommunicationException("Unexpected error when providing a document page for customerId=%s".formatted(customerId), e);
+ }
+ }
+
+ /**
+ * Get details gathered about the customer.
+ * @param customerId id of the customer.
+ * @param ownerId owner identification.
+ * @return optional of GetCustomerResponse with details about the customer.
+ * @throws RemoteCommunicationException in case of 4xx or 5xx response status code.
+ */
+ public GetCustomerResponse getCustomer(final String customerId, final OwnerId ownerId) throws RemoteCommunicationException {
+ final String apiPath = "/api/v1/customers/%s".formatted(customerId);
+
+ try {
+ logger.info("Getting details about customer {}, {}", customerId, ownerId);
+ logger.debug("Calling {}", apiPath);
+ final ResponseEntity response = restClient.get(apiPath, new ParameterizedTypeReference<>() {});
+ logger.info("Got {} for getting details about customer, {}", response.getStatusCode(), ownerId);
+ logger.debug("{} response status code: {}", apiPath, response.getStatusCode());
+ logger.trace("{} response: {}", apiPath, response);
+ return response.getBody();
+ } catch (RestClientException e) {
+ throw new RemoteCommunicationException("REST API call failed when getting details of customerId=%s, statusCode=%s, responseBody='%s'".formatted(customerId, e.getStatusCode(), e.getResponse()), e);
+ } catch (Exception e) {
+ throw new RemoteCommunicationException("Unexpected error when getting details of customerId=%s".formatted(customerId), e);
+ }
+ }
+
+ /**
+ * Get document portrait of the customer.
+ * @param customerId id of the customer.
+ * @param ownerId owner identification.
+ * @return successful Response contains a base64 in the JPG format.
+ * @throws RemoteCommunicationException in case of 4xx or 5xx response status code.
+ */
+ public Optional getDocumentPortrait(final String customerId, final OwnerId ownerId) throws RemoteCommunicationException {
+ final String apiPath = "/api/v1/customers/%s/document/portrait".formatted(customerId);
+
+ try {
+ logger.info("Getting document portrait of customer {}, {}", customerId, ownerId);
+ logger.debug("Calling {}", apiPath);
+ final ResponseEntity response = restClient.get(apiPath, new ParameterizedTypeReference<>() {});
+ logger.info("Got {} for getting document portrait, {}", response.getStatusCode(), ownerId);
+ logger.debug("{} response status code: {}", apiPath, response.getStatusCode());
+ logger.trace("{} response: {}", apiPath, response);
+ return Optional.ofNullable(response.getBody());
+ } catch (RestClientException e) {
+ if (HttpStatus.NOT_FOUND == e.getStatusCode()) {
+ // API returns 404 in case of missing portrait photo.
+ logger.debug("Missing portrait photo for customer {}, {}", customerId, ownerId);
+ return Optional.empty();
+ }
+ throw new RemoteCommunicationException("REST API call failed when getting customer portrait, statusCode=%s, responseBody='%s'".formatted(e.getStatusCode(), e.getResponse()), e);
+ }
+ }
+
+ /**
+ * Inspect consistency of data of the submitted document provided for a customer.
+ * @param customerId id of the customer whose document to inspect.
+ * @param ownerId owner identification.
+ * @return optional of DocumentInspectResponse with details about consistency of the document.
+ * @throws RemoteCommunicationException in case of 4xx or 5xx response status code.
+ */
+ public DocumentInspectResponse inspectDocument(final String customerId, final OwnerId ownerId) throws RemoteCommunicationException {
+ final String apiPath = "/api/v1/customers/%s/document/inspect".formatted(customerId);
+
+ try {
+ logger.info("Getting document inspect of customer {}, {}", customerId, ownerId);
+ logger.debug("Calling {}", apiPath);
+ final ResponseEntity response = restClient.post(apiPath, null, new ParameterizedTypeReference<>() {});
+ logger.info("Got {} for getting document inspect, {}", response.getStatusCode(), ownerId);
+ logger.debug("{} response status code: {}", apiPath, response.getStatusCode());
+ logger.trace("{} response: {}", apiPath, response);
+ return response.getBody();
+ } catch (RestClientException e) {
+ throw new RemoteCommunicationException("REST API call failed while getting document inspection for customerId=%s, statusCode=%s, responseBody='%s'".formatted(customerId, e.getStatusCode(), e.getResponse()), e);
+ } catch (Exception e) {
+ throw new RemoteCommunicationException("Unexpected error when getting document inspection for customerId=%s".formatted(customerId), e);
+ }
+ }
+
+ /**
+ * Delete customer.
+ * @param customerId id of the customer.
+ * @param ownerId owner identification.
+ * @throws RemoteCommunicationException in case of 4xx or 5xx response status code.
+ */
+ public void deleteCustomer(final String customerId, final OwnerId ownerId) throws RemoteCommunicationException {
+ final String apiPath = "/api/v1/customers/%s".formatted(customerId);
+
+ try {
+ logger.info("Deleting customer {}, {}", customerId, ownerId);
+ logger.debug("Calling {}", apiPath);
+ final ResponseEntity response = restClient.delete(apiPath, new ParameterizedTypeReference<>() {});
+ logger.info("Got {} for deleting customer, {}", response.getStatusCode(), ownerId);
+ logger.debug("{} response status code: {}", apiPath, response.getStatusCode());
+ logger.trace("{} response: {}", apiPath, response);
+ } catch (RestClientException e) {
+ throw new RemoteCommunicationException("REST API call failed when deleting customer, statusCode=%s, responseBody='%s'".formatted(e.getStatusCode(), e.getResponse()), e);
+ }
+ }
+
+ /**
+ * Delete customer's document.
+ * @param customerId id of the customer.
+ * @param ownerId owner identification.
+ * @throws RemoteCommunicationException in case of 4xx or 5xx response status code.
+ */
+ public void deleteDocument(final String customerId, final OwnerId ownerId) throws RemoteCommunicationException {
+ final String apiPath = "/api/v1/customers/%s/document".formatted(customerId);
+
+ try {
+ logger.info("Deleting document of customer {}, {}", customerId, ownerId);
+ logger.debug("Calling {}", apiPath);
+ final ResponseEntity response = restClient.delete(apiPath, new ParameterizedTypeReference<>() {});
+ logger.info("Got {} for deleting customer's document, {}", response.getStatusCode(), ownerId);
+ logger.debug("{} response status code: {}", apiPath, response.getStatusCode());
+ logger.trace("{} response: {}", apiPath, response);
+ } catch (RestClientException e) {
+ throw new RemoteCommunicationException("REST API call failed when deleting customer's document, statusCode=%s, responseBody='%s'".formatted(e.getStatusCode(), e.getResponse()), e);
+ }
+ }
+
+ /**
+ * Converts internal DocumentType enum to string value used by Innovatrics.
+ * @param type represents type of document.
+ * @return document type as a string value.
+ */
+ private static String convertType(DocumentType type) throws DocumentVerificationException {
+ return switch (type) {
+ case ID_CARD -> "identity-card";
+ case PASSPORT -> "passport";
+ case DRIVING_LICENSE -> "drivers-licence";
+ default -> throw new DocumentVerificationException("Unsupported documentType " + type);
+ };
+ }
+
+ /**
+ * Converts internal CardSide enum to string value used by Innovatrics.
+ * @param side represents side of a card.
+ * @return side of a card as a string value.
+ */
+ private static String convertSide(CardSide side) {
+ return switch (side) {
+ case FRONT -> "front";
+ case BACK -> "back";
+ };
+ }
+
}
diff --git a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsConfigProps.java b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsConfigProps.java
index 62fb7f67c..ce0acde45 100644
--- a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsConfigProps.java
+++ b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsConfigProps.java
@@ -24,6 +24,9 @@
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
+import java.util.List;
+import java.util.Set;
+
/**
* Innovatrics configuration properties.
*
@@ -62,6 +65,8 @@ class InnovatricsConfigProps {
private PresenceCheckConfiguration presenceCheckConfiguration;
+ private DocumentVerificationConfiguration documentVerificationConfiguration;
+
@Getter @Setter
public static class PresenceCheckConfiguration {
/**
@@ -70,4 +75,17 @@ public static class PresenceCheckConfiguration {
private double score = 0.875;
}
+ @Getter @Setter
+ public static class DocumentVerificationConfiguration {
+ /**
+ * Identifies expected document countries of issue in ISO 3166-1 alpha-3 format.
+ */
+ private List documentCountries;
+
+ /**
+ * Set of fields in camelCase that are cross-validated between the machine-readable zone and visual zone, if extracted.
+ */
+ private Set crucialFields;
+ }
+
}
diff --git a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsDocumentVerificationProvider.java b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsDocumentVerificationProvider.java
index 70d177e73..e3d134135 100644
--- a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsDocumentVerificationProvider.java
+++ b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsDocumentVerificationProvider.java
@@ -17,35 +17,100 @@
*/
package com.wultra.app.onboardingserver.provider.innovatrics;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Strings;
+import com.wultra.app.enrollmentserver.model.enumeration.DocumentType;
+import com.wultra.app.enrollmentserver.model.enumeration.DocumentVerificationStatus;
import com.wultra.app.enrollmentserver.model.integration.*;
+import com.wultra.app.enrollmentserver.model.integration.Image;
import com.wultra.app.onboardingserver.api.errorhandling.DocumentVerificationException;
import com.wultra.app.onboardingserver.api.provider.DocumentVerificationProvider;
import com.wultra.app.onboardingserver.common.database.entity.DocumentResultEntity;
import com.wultra.app.onboardingserver.common.database.entity.DocumentVerificationEntity;
import com.wultra.app.onboardingserver.common.errorhandling.RemoteCommunicationException;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import com.wultra.app.onboardingserver.provider.innovatrics.model.api.*;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Component;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
+import java.util.stream.Collectors;
/**
* Implementation of the {@link DocumentVerificationProvider} with Innovatrics.
*
* @author Jan Pesek, jan.pesek@wultra.com
*/
-@ConditionalOnProperty(value = "enrollment-server-onboarding.document-verification.provider", havingValue = "innovatrics")
+@ConditionalOnExpression("""
+ '${enrollment-server-onboarding.presence-check.provider}' == 'innovatrics' and '${enrollment-server-onboarding.document-verification.provider}' == 'innovatrics'
+ """)
@Component
-class InnovatricsDocumentVerificationProvider implements DocumentVerificationProvider {
+@AllArgsConstructor
+@Slf4j
+public class InnovatricsDocumentVerificationProvider implements DocumentVerificationProvider {
+
+ private final InnovatricsApiService innovatricsApiService;
+ private final ObjectMapper objectMapper;
+ private final InnovatricsConfigProps configuration;
@Override
public DocumentsSubmitResult checkDocumentUpload(OwnerId id, DocumentVerificationEntity document) throws RemoteCommunicationException, DocumentVerificationException {
- return null;
+ logger.warn("Unexpected state of document {}, {}", document, id);
+ throw new UnsupportedOperationException("Method checkDocumentUpload is not supported by Innovatrics provider.");
}
@Override
public DocumentsSubmitResult submitDocuments(OwnerId id, List documents) throws RemoteCommunicationException, DocumentVerificationException {
- return null;
+ if (CollectionUtils.isEmpty(documents)) {
+ logger.info("Empty documents list passed to document provider, {}", id);
+ return new DocumentsSubmitResult();
+ }
+
+ final DocumentType documentType = documents.get(0).getType();
+ if (DocumentType.SELFIE_PHOTO == documentType) {
+ logger.debug("Selfie photo passed as a document, {}", id);
+ throw new DocumentVerificationException("Selfie photo cannot be submitted as a document");
+ }
+
+ if (DocumentType.SELFIE_VIDEO == documentType) {
+ logger.debug("Selfie video passed as a document, {}", id);
+ throw new DocumentVerificationException("Selfie video cannot be submitted as a document");
+ }
+
+ final String customerId = createCustomer(id);
+ createDocument(customerId, documentType, id);
+ logger.debug("Created new customer {}, {}", customerId, id);
+
+ final DocumentsSubmitResult results = new DocumentsSubmitResult();
+ for (SubmittedDocument page : documents) {
+ final CreateDocumentPageResponse createDocumentPageResponse = provideDocumentPage(customerId, page, id);
+ if (containsError(createDocumentPageResponse)) {
+ logger.debug("Page upload was not successful, {}", id);
+ results.getResults().add(createErrorSubmitResult(customerId, createDocumentPageResponse, page));
+ } else {
+ logger.debug("Document page was read successfully by provider, {}", id);
+ results.getResults().add(createSubmitResult(customerId, page));
+ }
+ }
+
+ final Optional primaryPage = results.getResults().stream()
+ .filter(result -> Strings.isNullOrEmpty(result.getRejectReason()) && Strings.isNullOrEmpty(result.getErrorDetail()))
+ .findFirst();
+
+ if (primaryPage.isPresent()) {
+ // Only first found successfully submitted page has extracted data, others has empty JSON
+ primaryPage.get().setExtractedData(getExtractedData(customerId, id));
+ if (hasDocumentPortrait(customerId, id)) {
+ results.setExtractedPhotoId(customerId);
+ }
+ }
+
+ return results;
}
@Override
@@ -55,31 +120,303 @@ public boolean shouldStoreSelfie() {
@Override
public DocumentsVerificationResult verifyDocuments(OwnerId id, List uploadIds) throws RemoteCommunicationException, DocumentVerificationException {
- return null;
+ final DocumentsVerificationResult results = new DocumentsVerificationResult();
+ results.setResults(new ArrayList<>());
+
+ // Pages of the same document have same uploadId (= customerId), no reason to generate verification for each one.
+ final List distinctUploadIds = uploadIds.stream().distinct().toList();
+ for (String customerId : distinctUploadIds) {
+ final DocumentVerificationResult result = createVerificationResult(customerId, id);
+ results.getResults().add(result);
+ }
+
+ final String rejectReasons = results.getResults().stream()
+ .map(DocumentVerificationResult::getRejectReason)
+ .filter(StringUtils::hasText)
+ .collect(Collectors.joining(";"));
+ if (StringUtils.hasText(rejectReasons)) {
+ logger.debug("Some documents were rejected: rejectReasons={}, {}", rejectReasons, id);
+ results.setStatus(DocumentVerificationStatus.REJECTED);
+ results.setRejectReason(rejectReasons);
+ } else {
+ logger.debug("All documents accepted, {}", id);
+ results.setStatus(DocumentVerificationStatus.ACCEPTED);
+ }
+ results.setVerificationId(UUID.randomUUID().toString());
+ return results;
}
@Override
public DocumentsVerificationResult getVerificationResult(OwnerId id, String verificationId) throws RemoteCommunicationException, DocumentVerificationException {
- return null;
+ logger.warn("Unexpected state of documents with verificationId={}, {}", verificationId, id);
+ throw new UnsupportedOperationException("Method getVerificationResult is not supported by Innovatrics provider.");
}
@Override
public Image getPhoto(String photoId) throws RemoteCommunicationException, DocumentVerificationException {
- return null;
+ logger.warn("Unexpected document portrait query for customerId={}", photoId);
+ throw new UnsupportedOperationException("Method getPhoto is not implemented by Innovatrics provider.");
}
@Override
public void cleanupDocuments(OwnerId id, List uploadIds) throws RemoteCommunicationException, DocumentVerificationException {
-
+ // Pages of the same document have same uploadId (= customerId), no reason to call delete for each one.
+ final List distinctUploadIds = uploadIds.stream().distinct().toList();
+ logger.info("Invoked cleanupDocuments, {}", id);
+ for (String customerId : distinctUploadIds) {
+ innovatricsApiService.deleteCustomer(customerId, id);
+ }
}
@Override
public List parseRejectionReasons(DocumentResultEntity docResult) throws DocumentVerificationException {
- return null;
+ logger.debug("Parsing rejection reasons of {}", docResult);
+ final String rejectionReasons = docResult.getRejectReason();
+ if (!StringUtils.hasText(rejectionReasons)) {
+ return Collections.emptyList();
+ }
+
+ return deserializeFromString(rejectionReasons);
}
@Override
public VerificationSdkInfo initVerificationSdk(OwnerId id, Map initAttributes) throws RemoteCommunicationException, DocumentVerificationException {
- return null;
+ logger.debug("#initVerificationSdk does nothing for Innovatrics, {}", id);
+ return new VerificationSdkInfo();
+ }
+
+ /**
+ * Create a new customer resource.
+ * @param ownerId owner identification.
+ * @return ID of the new customer.
+ * @throws RemoteCommunicationException if the resource was not created properly.
+ */
+ private String createCustomer(final OwnerId ownerId) throws RemoteCommunicationException {
+ return innovatricsApiService.createCustomer(ownerId).getId();
+ }
+
+ /**
+ * Create a new document resource to an existing customer.
+ * @param customerId id of the customer to assign the resource to.
+ * @param documentType type of the document that will be uploaded later.
+ * @param ownerId owner identification.
+ * @throws RemoteCommunicationException if the resource was not created properly.
+ */
+ private void createDocument(final String customerId, final DocumentType documentType, final OwnerId ownerId) throws RemoteCommunicationException, DocumentVerificationException {
+ innovatricsApiService.createDocument(customerId, documentType, ownerId);
+ }
+
+ /**
+ * Upload a page of a document to a customer.
+ * @param customerId id of the customer to whom upload the document page.
+ * @param page SubmittedDocument object representing the page.
+ * @param ownerId owner identification.
+ * @return CreateDocumentPageResponse containing info about the document type. An unsuccessful response will contain an error code.
+ * @throws RemoteCommunicationException if the document page was not uploaded properly.
+ */
+ private CreateDocumentPageResponse provideDocumentPage(final String customerId, final SubmittedDocument page, final OwnerId ownerId) throws RemoteCommunicationException {
+ return innovatricsApiService.provideDocumentPage(customerId, page.getSide(), page.getPhoto().getData(), ownerId);
+ }
+
+ /**
+ * Checks if CreateDocumentPageResponse contains error or warnings.
+ * @param pageResponse response to a page upload.
+ * @return true if there is an error or warnings, false otherwise.
+ */
+ private static boolean containsError(CreateDocumentPageResponse pageResponse) {
+ return pageResponse.getErrorCode() != null || !CollectionUtils.isEmpty(pageResponse.getWarnings());
+ }
+
+ /**
+ * Creates DocumentSubmitResult with error or reject reason.
+ * @param uploadId external id of the document.
+ * @param response returned from provider.
+ * @return DocumentSubmitResult with error or reject reason.
+ * @throws DocumentVerificationException in case of rejection reason serialization error.
+ */
+ private DocumentSubmitResult createErrorSubmitResult(String uploadId, CreateDocumentPageResponse response, SubmittedDocument submitted) throws DocumentVerificationException {
+ final DocumentSubmitResult result = new DocumentSubmitResult();
+ result.setUploadId(uploadId);
+ result.setDocumentId(submitted.getDocumentId());
+
+ final List rejectionReasons = new ArrayList<>();
+ if (response.getErrorCode() != null) {
+ switch (response.getErrorCode()) {
+ case NO_CARD_CORNERS_DETECTED -> rejectionReasons.add("Document page was not detected in the photo.");
+ case PAGE_DOESNT_MATCH_DOCUMENT_TYPE_OF_PREVIOUS_PAGE -> rejectionReasons.add("Mismatched document pages types.");
+ default -> rejectionReasons.add("Unknown error: %s".formatted(response.getErrorCode().getValue()));
+ }
+ }
+
+ if (!CollectionUtils.isEmpty(response.getWarnings())) {
+ for (CreateDocumentPageResponse.WarningsEnum warning : response.getWarnings()) {
+ switch (warning) {
+ case DOCUMENT_TYPE_NOT_RECOGNIZED -> rejectionReasons.add("Document type not recognized.");
+ default -> rejectionReasons.add("Unknown warning: %s".formatted(warning.getValue()));
+ }
+ }
+ }
+
+ if (!rejectionReasons.isEmpty()) {
+ result.setRejectReason(serializeToString(rejectionReasons));
+ }
+
+ return result;
+ }
+
+ /**
+ * Gets all customer data extracted from uploaded documents in a JSON form.
+ * @param customerId id of the customer.
+ * @param ownerId owner identification.
+ * @return JSON serialized data.
+ * @throws RemoteCommunicationException in case of the remote service error.
+ * @throws DocumentVerificationException if the returned data could not be provided.
+ */
+ private String getExtractedData(final String customerId, final OwnerId ownerId) throws RemoteCommunicationException, DocumentVerificationException {
+ return serializeToString(innovatricsApiService.getCustomer(customerId, ownerId));
}
+
+ /**
+ * Checks if a document portrait of the customer is available.
+ * @param customerId id of the customer.
+ * @param ownerId owner identification.
+ * @return true if document portrait is available, false otherwise.
+ */
+ private boolean hasDocumentPortrait(final String customerId, final OwnerId ownerId) throws RemoteCommunicationException {
+ return innovatricsApiService.getDocumentPortrait(customerId, ownerId).isPresent();
+ }
+
+ /**
+ * Creates DocumentSubmitResult containing extracted data.
+ * @param customerId id of the customer to get data from.
+ * @return DocumentSubmitResult containing extracted data.
+ */
+ private static DocumentSubmitResult createSubmitResult(final String customerId, final SubmittedDocument submitted) {
+ final DocumentSubmitResult result = new DocumentSubmitResult();
+ result.setUploadId(customerId);
+ result.setDocumentId(submitted.getDocumentId());
+ result.setExtractedData(DocumentSubmitResult.NO_DATA_EXTRACTED);
+ return result;
+ }
+
+ /**
+ * Gets document inspection from Innovatrics and parses it to the verification result.
+ * @param customerId id of the customer the document belongs to.
+ * @param ownerId owner identification.
+ * @return DocumentVerificationResult
+ */
+ private DocumentVerificationResult createVerificationResult(String customerId, OwnerId ownerId) throws DocumentVerificationException, RemoteCommunicationException {
+ final DocumentInspectResponse response = innovatricsApiService.inspectDocument(customerId, ownerId);
+
+ final List rejectionReasons = new ArrayList<>();
+ if (Boolean.TRUE.equals(response.getExpired())) {
+ rejectionReasons.add("Document expired.");
+ }
+
+ if (response.getMrzInspection() != null && !Boolean.TRUE.equals(response.getMrzInspection().getValid())) {
+ rejectionReasons.add("MRZ does not conform the ICAO specification.");
+ }
+
+ final VisualZoneInspection viz = response.getVisualZoneInspection();
+ rejectionReasons.addAll(parseVisualZoneInspection(viz));
+
+ if (response.getPageTampering() != null) {
+ response.getPageTampering().forEach((side, inspection) -> {
+ if (Boolean.TRUE.equals(inspection.getColorProfileChangeDetected())) {
+ rejectionReasons.add("Colors on the document %s does not corresponds to the expected color profile.".formatted(side));
+ }
+ if (Boolean.TRUE.equals(inspection.getLooksLikeScreenshot())) {
+ rejectionReasons.add("Provided image of the document %s was taken from a screen of another device.".formatted(side));
+ }
+ if (Boolean.TRUE.equals(inspection.getTamperedTexts())) {
+ rejectionReasons.add("Text of the document %s is tampered.".formatted(side));
+ }
+ });
+ }
+
+ final DocumentVerificationResult result = new DocumentVerificationResult();
+ result.setUploadId(customerId);
+ result.setVerificationResult(serializeToString(response));
+ if (!rejectionReasons.isEmpty()) {
+ result.setRejectReason(serializeToString(rejectionReasons));
+ }
+ return result;
+ }
+
+ /**
+ * Parse VisualZoneInspection of a document provided by Innovatrics.
+ * @param visualZoneInspection inspection of a document by Innovatrics.
+ * @return List of reasons to reject the document.
+ */
+ private List parseVisualZoneInspection(final VisualZoneInspection visualZoneInspection) {
+ final List rejectionReasons = new ArrayList<>();
+ if (visualZoneInspection == null) {
+ return rejectionReasons;
+ }
+
+ // Contains fields with a ocr confidence lower than ocr-text-field-threshold settings.
+ final List lowOcrConfidenceAttributes = visualZoneInspection.getOcrConfidence().getLowOcrConfidenceTexts();
+ if (!CollectionUtils.isEmpty(lowOcrConfidenceAttributes)) {
+ rejectionReasons.add("Low OCR confidence of attributes: %s".formatted(lowOcrConfidenceAttributes));
+ }
+
+ final TextConsistency textConsistency = visualZoneInspection.getTextConsistency();
+ if (textConsistency == null) {
+ return rejectionReasons;
+ }
+
+ final TextConsistentWith textConsistentWith = textConsistency.getConsistencyWith();
+ if (textConsistentWith == null) {
+ return rejectionReasons;
+ }
+
+ final MrzConsistency mrzConsistency = textConsistentWith.getMrz();
+ if (mrzConsistency != null) {
+ final List inconsistentAttributes = getCrucial(mrzConsistency.getInconsistentTexts());
+ if (!inconsistentAttributes.isEmpty()) {
+ rejectionReasons.add("Inconsistent crucial attributes with MRZ: %s".formatted(inconsistentAttributes));
+ }
+ }
+
+ final BarcodesConsistency barcodesConsistency = textConsistentWith.getBarcodes();
+ if (barcodesConsistency != null) {
+ final List inconsistentAttributes = getCrucial(barcodesConsistency.getInconsistentTexts());
+ if (!inconsistentAttributes.isEmpty()) {
+ rejectionReasons.add("Inconsistent crucial attributes with barcode: %s".formatted(inconsistentAttributes));
+ }
+ }
+
+ return rejectionReasons;
+ }
+
+ /**
+ * Intersects list of attributes with CRUCIAL_ATTRIBUTES.
+ * @param attributes Attributes to do the intersection on.
+ * @return Attributes intersection.
+ */
+ private List getCrucial(final List attributes) {
+ if (attributes == null) {
+ return Collections.emptyList();
+ }
+
+ final Set crucialFields = configuration.getDocumentVerificationConfiguration().getCrucialFields();
+ return attributes.stream().filter(crucialFields::contains).toList();
+ }
+
+ private String serializeToString(T src) throws DocumentVerificationException {
+ try {
+ return objectMapper.writeValueAsString(src);
+ } catch (JsonProcessingException e) {
+ throw new DocumentVerificationException("Unexpected error when serializing data", e);
+ }
+ }
+
+ private T deserializeFromString(String src) throws DocumentVerificationException {
+ try {
+ return objectMapper.readValue(src, new TypeReference<>() {});
+ } catch (JsonProcessingException e) {
+ throw new DocumentVerificationException("Unexpected error when deserializing data", e);
+ }
+ }
+
}
diff --git a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsLivenessService.java b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsLivenessService.java
index 833396add..8b97aa52e 100644
--- a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsLivenessService.java
+++ b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsLivenessService.java
@@ -33,7 +33,7 @@
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -46,11 +46,11 @@
@Transactional(readOnly = true)
@Slf4j
@AllArgsConstructor
-@ConditionalOnProperty(value = "enrollment-server-onboarding.presence-check.provider", havingValue = "innovatrics")
+@ConditionalOnExpression("""
+ '${enrollment-server-onboarding.presence-check.provider}' == 'innovatrics' and ${enrollment-server-onboarding.onboarding-process.enabled} == true
+ """)
class InnovatricsLivenessService {
- private static final String INNOVATRICS_CUSTOMER_ID = "InnovatricsCustomerId";
-
private final InnovatricsApiService innovatricsApiService;
private final IdentityVerificationRepository identityVerificationRepository;
@@ -124,8 +124,7 @@ private static String fetchCustomerId(final OwnerId id, final IdentityVerificati
throw new IdentityVerificationException("Unable to deserialize session info", e);
}
- // TODO (racansky, 2023-11-28) discuss the format with Jan Pesek, extract to common logic
- final String customerId = (String) sessionInfo.getSessionAttributes().get(INNOVATRICS_CUSTOMER_ID);
+ final String customerId = (String) sessionInfo.getSessionAttributes().get(SessionInfo.ATTRIBUTE_PRIMARY_DOCUMENT_REFERENCE);
if (Strings.isNullOrEmpty(customerId)) {
throw new IdentityVerificationException("Missing a customer ID value for calling Innovatrics, " + id);
}
diff --git a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsPresenceCheckProvider.java b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsPresenceCheckProvider.java
index 7899e776c..007c06c6a 100644
--- a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsPresenceCheckProvider.java
+++ b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsPresenceCheckProvider.java
@@ -49,8 +49,6 @@
@AllArgsConstructor
class InnovatricsPresenceCheckProvider implements PresenceCheckProvider {
- private static final String INNOVATRICS_CUSTOMER_ID = "InnovatricsCustomerId";
-
private final InnovatricsApiService innovatricsApiService;
private final InnovatricsConfigProps configuration;
@@ -61,8 +59,8 @@ public void initPresenceCheck(final OwnerId id, final Image photo) {
}
@Override
- public boolean shouldProvideTrustedPhoto() {
- return false;
+ public TrustedPhotoSource trustedPhotoSource() {
+ return TrustedPhotoSource.REFERENCE;
}
@Override
@@ -152,8 +150,7 @@ private static Optional fail(final String errorDetail) {
}
private static String fetchCustomerId(final OwnerId id, final SessionInfo sessionInfo) throws PresenceCheckException {
- // TODO (racansky, 2023-11-28) discuss the format with Jan Pesek
- final String customerId = (String) sessionInfo.getSessionAttributes().get(INNOVATRICS_CUSTOMER_ID);
+ final String customerId = (String) sessionInfo.getSessionAttributes().get(SessionInfo.ATTRIBUTE_PRIMARY_DOCUMENT_REFERENCE);
if (Strings.isNullOrEmpty(customerId)) {
throw new PresenceCheckException("Missing a customer ID value for calling Innovatrics, " + id);
}
@@ -164,12 +161,7 @@ private static String fetchCustomerId(final OwnerId id, final SessionInfo sessio
public void cleanupIdentityData(final OwnerId id, final SessionInfo sessionInfo) throws PresenceCheckException, RemoteCommunicationException {
logger.info("Invoked cleanupIdentityData, {}", id);
final String customerId = fetchCustomerId(id, sessionInfo);
-
- innovatricsApiService.deleteLiveness(customerId, id);
- logger.debug("Deleted liveness, {}", id);
-
- innovatricsApiService.deleteSelfie(customerId, id);
- logger.debug("Deleted selfie, {}", id);
+ innovatricsApiService.deleteCustomer(customerId, id);
}
record PresenceCheckError(PresenceCheckStatus status, String rejectReason, String errorDetail){}
diff --git a/enrollment-server-onboarding-provider-innovatrics/src/test/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsDocumentVerificationProviderTest.java b/enrollment-server-onboarding-provider-innovatrics/src/test/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsDocumentVerificationProviderTest.java
new file mode 100644
index 000000000..fa9384eba
--- /dev/null
+++ b/enrollment-server-onboarding-provider-innovatrics/src/test/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsDocumentVerificationProviderTest.java
@@ -0,0 +1,174 @@
+/*
+ * PowerAuth Enrollment Server
+ * Copyright (C) 2023 Wultra s.r.o.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.wultra.app.onboardingserver.provider.innovatrics;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.jayway.jsonpath.JsonPath;
+import com.wultra.app.enrollmentserver.model.enumeration.CardSide;
+import com.wultra.app.enrollmentserver.model.enumeration.DocumentType;
+import com.wultra.app.enrollmentserver.model.enumeration.DocumentVerificationStatus;
+import com.wultra.app.enrollmentserver.model.integration.*;
+import com.wultra.app.enrollmentserver.model.integration.Image;
+import com.wultra.app.onboardingserver.common.database.entity.DocumentResultEntity;
+import com.wultra.app.onboardingserver.provider.innovatrics.model.api.*;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.util.StringUtils;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * Test of {@link InnovatricsDocumentVerificationProvider}.
+ *
+ * @author Jan Pesek, jan.pesek@wultra.com
+ */
+@SpringBootTest(classes = EnrollmentServerTestApplication.class)
+@ActiveProfiles("test")
+class InnovatricsDocumentVerificationProviderTest {
+
+ @Autowired
+ private InnovatricsDocumentVerificationProvider tested;
+
+ @MockBean
+ private InnovatricsApiService apiService;
+
+ @Test
+ void testSubmitDocuments() throws Exception {
+ final OwnerId ownerId = createOwnerId();
+ when(apiService.createCustomer(ownerId)).thenReturn(new CreateCustomerResponse("c123"));
+
+ final Links docLink = new Links("docResource");
+ final CreateDocumentResponse documentResponse = new CreateDocumentResponse();
+ documentResponse.setLinks(docLink);
+ when(apiService.createDocument("c123", DocumentType.PASSPORT, ownerId)).thenReturn(documentResponse);
+
+ final CreateDocumentPageResponse pageResponse = new CreateDocumentPageResponse();
+ when(apiService.provideDocumentPage("c123", CardSide.FRONT, "img".getBytes(), ownerId)).thenReturn(pageResponse);
+
+ final BiometricMultiValueAttribute ageAttr = new BiometricMultiValueAttribute("42", null, null, null, "40");
+ final MultiValueAttribute surnameAttr = new MultiValueAttribute("SPECIMEN", null, null, null);
+ final Customer customer = new Customer();
+ customer.age(ageAttr).setSurname(surnameAttr);
+ final GetCustomerResponse customerResponse = new GetCustomerResponse();
+ customerResponse.customer(customer);
+ when(apiService.getCustomer("c123", ownerId)).thenReturn(customerResponse);
+
+ final SubmittedDocument doc = new SubmittedDocument();
+ doc.setType(DocumentType.PASSPORT);
+ doc.setSide(CardSide.FRONT);
+ doc.setPhoto(Image.builder().data("img".getBytes()).build());
+
+ final DocumentsSubmitResult results = tested.submitDocuments(ownerId, List.of(doc));
+ verify(apiService).getCustomer("c123", ownerId);
+ assertEquals(1, results.getResults().size());
+
+ final DocumentSubmitResult result = results.getResults().get(0);
+ assertEquals("c123", result.getUploadId());
+ assertFalse(StringUtils.hasText(result.getErrorDetail()));
+ assertFalse(StringUtils.hasText(result.getRejectReason()));
+ assertNotNull(result.getExtractedData());
+
+ assertEquals("42", JsonPath.read(result.getExtractedData(), "$.customer.age.visualZone"));
+ assertEquals("40", JsonPath.read(result.getExtractedData(), "$.customer.age.documentPortrait"));
+ assertEquals("SPECIMEN", JsonPath.read(result.getExtractedData(), "$.customer.surname.visualZone"));
+ }
+
+ @Test
+ void testSubmitDocument_handleProvideDocumentPageError() throws Exception {
+ final OwnerId ownerId = createOwnerId();
+ when(apiService.createCustomer(ownerId)).thenReturn(new CreateCustomerResponse("c123"));
+
+ final Links docLink = new Links("docResource");
+ final CreateDocumentResponse documentResponse = new CreateDocumentResponse();
+ documentResponse.setLinks(docLink);
+ when(apiService.createDocument("c123", DocumentType.PASSPORT, ownerId)).thenReturn(documentResponse);
+
+ final CreateDocumentPageResponse pageResponse = new CreateDocumentPageResponse(
+ "front",
+ CreateDocumentPageResponse.ErrorCodeEnum.NO_CARD_CORNERS_DETECTED,
+ List.of(CreateDocumentPageResponse.WarningsEnum.DOCUMENT_TYPE_NOT_RECOGNIZED));
+ when(apiService.provideDocumentPage("c123", CardSide.FRONT, "img".getBytes(), ownerId)).thenReturn(pageResponse);
+
+ final SubmittedDocument doc = new SubmittedDocument();
+ doc.setType(DocumentType.PASSPORT);
+ doc.setSide(CardSide.FRONT);
+ doc.setPhoto(Image.builder().data("img".getBytes()).build());
+
+ final DocumentsSubmitResult results = tested.submitDocuments(ownerId, List.of(doc));
+ verify(apiService).provideDocumentPage("c123", CardSide.FRONT, "img".getBytes(), ownerId);
+ assertEquals(1, results.getResults().size());
+
+ final DocumentSubmitResult result = results.getResults().get(0);
+ assertEquals("c123", result.getUploadId());
+ assertTrue(StringUtils.hasText(result.getRejectReason()));
+ }
+
+ @Test
+ void testParseRejectionReason() throws Exception {
+ final DocumentResultEntity entity = new DocumentResultEntity();
+ entity.setRejectReason(new ObjectMapper().writeValueAsString(List.of("Reason1", "Reason2")));
+ assertEquals(List.of("Reason1", "Reason2"), tested.parseRejectionReasons(entity));
+ }
+
+ @Test
+ void testParseEmptyRejectionReason() throws Exception {
+ final DocumentResultEntity entity = new DocumentResultEntity();
+ assertTrue(tested.parseRejectionReasons(entity).isEmpty());
+ }
+
+ @Test
+ void testVerifyDocuments() throws Exception {
+ final OwnerId ownerId = createOwnerId();
+ final DocumentInspectResponse response = new DocumentInspectResponse();
+ when(apiService.inspectDocument("c123", ownerId)).thenReturn(response);
+
+ final DocumentsVerificationResult result = tested.verifyDocuments(ownerId, List.of("c123"));
+ assertTrue(result.isAccepted());
+ assertEquals("c123", result.getResults().get(0).getUploadId());
+ assertNotNull(result.getVerificationId());
+ assertNotNull(result.getResults().get(0).getVerificationResult());
+ }
+
+ @Test
+ void testVerifyDocuments_expired() throws Exception {
+ final OwnerId ownerId = createOwnerId();
+ final DocumentInspectResponse response = new DocumentInspectResponse(true, null);
+ when(apiService.inspectDocument("c123", ownerId)).thenReturn(response);
+
+ final DocumentsVerificationResult result = tested.verifyDocuments(ownerId, List.of("c123"));
+ assertEquals(DocumentVerificationStatus.REJECTED, result.getStatus());
+ assertEquals(List.of("Document expired."), new ObjectMapper().readValue(result.getRejectReason(), new TypeReference>() {}));
+ assertEquals("c123", result.getResults().get(0).getUploadId());
+ assertNotNull(result.getVerificationId());
+ }
+
+ private OwnerId createOwnerId() {
+ final OwnerId ownerId = new OwnerId();
+ ownerId.setUserId("joe");
+ ownerId.setActivationId("a123");
+ return ownerId;
+ }
+
+}
diff --git a/enrollment-server-onboarding-provider-innovatrics/src/test/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsPresenceCheckProviderTest.java b/enrollment-server-onboarding-provider-innovatrics/src/test/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsPresenceCheckProviderTest.java
index 7037ca408..ed57e1559 100644
--- a/enrollment-server-onboarding-provider-innovatrics/src/test/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsPresenceCheckProviderTest.java
+++ b/enrollment-server-onboarding-provider-innovatrics/src/test/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsPresenceCheckProviderTest.java
@@ -148,7 +148,7 @@ void testGetResult_customerInspectionRejected() throws Exception {
private static SessionInfo createSessionInfo() {
final SessionInfo sessionInfo = new SessionInfo();
- sessionInfo.setSessionAttributes(Map.of("InnovatricsCustomerId", CUSTOMER_ID));
+ sessionInfo.setSessionAttributes(Map.of("primaryDocumentReference", CUSTOMER_ID));
return sessionInfo;
}
diff --git a/enrollment-server-onboarding-provider-innovatrics/src/test/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsRestApiServiceTest.java b/enrollment-server-onboarding-provider-innovatrics/src/test/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsRestApiServiceTest.java
new file mode 100644
index 000000000..f6f065984
--- /dev/null
+++ b/enrollment-server-onboarding-provider-innovatrics/src/test/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsRestApiServiceTest.java
@@ -0,0 +1,138 @@
+/*
+ * PowerAuth Enrollment Server
+ * Copyright (C) 2023 Wultra s.r.o.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.wultra.app.onboardingserver.provider.innovatrics;
+
+import com.wultra.app.enrollmentserver.model.enumeration.CardSide;
+import com.wultra.app.enrollmentserver.model.integration.OwnerId;
+import com.wultra.app.onboardingserver.common.errorhandling.RemoteCommunicationException;
+import com.wultra.app.onboardingserver.provider.innovatrics.model.api.CreateCustomerResponse;
+import com.wultra.app.onboardingserver.provider.innovatrics.model.api.CreateDocumentPageResponse;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.test.context.ActiveProfiles;
+
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Test of {@link InnovatricsApiService}.
+ *
+ * @author Jan Pesek, jan.pesek@wultra.com
+ */
+@SpringBootTest(
+ classes = EnrollmentServerTestApplication.class,
+ properties = {
+ "enrollment-server-onboarding.provider.innovatrics.serviceBaseUrl=http://localhost:" + InnovatricsRestApiServiceTest.PORT
+ })
+@ActiveProfiles("test")
+class InnovatricsRestApiServiceTest {
+
+ static final int PORT = 52936;
+
+ @Autowired
+ private InnovatricsApiService tested;
+
+ private MockWebServer mockWebServer;
+
+ @BeforeEach
+ void setup() throws Exception {
+ mockWebServer = new MockWebServer();
+ mockWebServer.start(PORT);
+ }
+
+ @AfterEach
+ void cleanup() throws Exception {
+ mockWebServer.shutdown();
+ }
+
+ @Test
+ void testCreateCustomer() throws Exception {
+ final OwnerId ownerId = createOwnerId();
+ mockWebServer.enqueue(new MockResponse()
+ .setHeader("Content-Type", MediaType.APPLICATION_JSON)
+ // Real response to POST /api/v1/customers
+ .setBody("""
+ {
+ "id": "c2e91b1f-0ccb-4ba0-93ae-d255a2a443af",
+ "links": {
+ "self": "/api/v1/customers/c2e91b1f-0ccb-4ba0-93ae-d255a2a443af"
+ }
+ }
+ """)
+ .setResponseCode(HttpStatus.OK.value()));
+
+ final CreateCustomerResponse response = tested.createCustomer(ownerId);
+ assertEquals("c2e91b1f-0ccb-4ba0-93ae-d255a2a443af", response.getId());
+ assertEquals("/api/v1/customers/c2e91b1f-0ccb-4ba0-93ae-d255a2a443af", response.getLinks().getSelf());
+ }
+
+ @Test
+ void testErrorResponse() {
+ final OwnerId ownerId = createOwnerId();
+ mockWebServer.enqueue(new MockResponse()
+ .setHeader("Content-Type", MediaType.APPLICATION_JSON)
+ // Real response to uploading a page without previous document resource creation
+ .setBody("""
+ {
+ "errorCode": "NOT_FOUND",
+ "errorMessage": "string"
+ }
+ """)
+ .setResponseCode(500));
+
+ assertThrows(RemoteCommunicationException.class, () -> tested.createCustomer(ownerId));
+ }
+
+ @Test
+ void testNonMatchingPageType() throws Exception {
+ final OwnerId ownerId = createOwnerId();
+ mockWebServer.enqueue(new MockResponse()
+ .setHeader("Content-Type", MediaType.APPLICATION_JSON)
+ // Real response to uploading a second page that is different from the first one
+ .setBody("""
+ {
+ "errorCode": "PAGE_DOESNT_MATCH_DOCUMENT_TYPE_OF_PREVIOUS_PAGE"
+ }
+ """)
+ .setResponseCode(HttpStatus.OK.value()));
+
+ final CreateDocumentPageResponse response = tested.provideDocumentPage("123", CardSide.FRONT, "data".getBytes(), ownerId);
+ assertNotNull(response.getErrorCode());
+
+ final RecordedRequest recordedRequest = mockWebServer.takeRequest(1L, TimeUnit.SECONDS);
+ assertNotNull(recordedRequest);
+ assertEquals("PUT /api/v1/customers/123/document/pages HTTP/1.1", recordedRequest.getRequestLine());
+ }
+
+ private OwnerId createOwnerId() {
+ final OwnerId ownerId = new OwnerId();
+ ownerId.setUserId("joe");
+ ownerId.setActivationId("a123");
+ return ownerId;
+ }
+
+}
diff --git a/enrollment-server-onboarding-provider-innovatrics/src/test/resources/application-test.properties b/enrollment-server-onboarding-provider-innovatrics/src/test/resources/application-test.properties
new file mode 100644
index 000000000..8e68187cc
--- /dev/null
+++ b/enrollment-server-onboarding-provider-innovatrics/src/test/resources/application-test.properties
@@ -0,0 +1,8 @@
+enrollment-server-onboarding.document-verification.provider=innovatrics
+enrollment-server-onboarding.presence-check.provider=innovatrics
+enrollment-server-onboarding.onboarding-process.enabled=false
+
+enrollment-server-onboarding.provider.innovatrics.documentVerificationConfiguration.documentCountries=CZE,SVK
+enrollment-server-onboarding.provider.innovatrics.document-verification-configuration.crucialFields=documentNumber, dateOfIssue, dateOfExpiry, surname, dateOfBirth, personalNumber, givenNames
+
+enrollment-server-onboarding.provider.innovatrics.restClientConfig.maxInMemorySize=1048576
\ No newline at end of file
diff --git a/enrollment-server-onboarding-provider-iproov/src/main/java/com/wultra/app/onboardingserver/provider/iproov/IProovPresenceCheckProvider.java b/enrollment-server-onboarding-provider-iproov/src/main/java/com/wultra/app/onboardingserver/provider/iproov/IProovPresenceCheckProvider.java
index 8e85e5f66..d34579224 100644
--- a/enrollment-server-onboarding-provider-iproov/src/main/java/com/wultra/app/onboardingserver/provider/iproov/IProovPresenceCheckProvider.java
+++ b/enrollment-server-onboarding-provider-iproov/src/main/java/com/wultra/app/onboardingserver/provider/iproov/IProovPresenceCheckProvider.java
@@ -127,8 +127,8 @@ public void initPresenceCheck(final OwnerId id, final Image photo) throws Presen
}
@Override
- public boolean shouldProvideTrustedPhoto() {
- return true;
+ public TrustedPhotoSource trustedPhotoSource() {
+ return TrustedPhotoSource.IMAGE;
}
@Override
diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/mock/provider/WultraMockDocumentVerificationProvider.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/mock/provider/WultraMockDocumentVerificationProvider.java
index bddd9e43c..692830985 100644
--- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/mock/provider/WultraMockDocumentVerificationProvider.java
+++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/mock/provider/WultraMockDocumentVerificationProvider.java
@@ -104,7 +104,11 @@ public DocumentsSubmitResult checkDocumentUpload(OwnerId id, DocumentVerificatio
}
@Override
- public DocumentsSubmitResult submitDocuments(OwnerId id, List documents) {
+ public DocumentsSubmitResult submitDocuments(OwnerId id, List documents) throws DocumentVerificationException {
+ if (documents.stream().anyMatch(doc -> "throw.exception".equals(doc.getPhoto().getFilename()))) {
+ throw new DocumentVerificationException("Filename to throw an exception is present in documents.");
+ }
+
final List submitResults = documents.stream()
.map(this::toDocumentSubmitResult)
.toList();
diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckService.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckService.java
index 86323a35f..12471f923 100644
--- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckService.java
+++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckService.java
@@ -44,10 +44,7 @@
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
-import java.util.Date;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
+import java.util.*;
import static com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationPhase.PRESENCE_CHECK;
import static com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationStatus.*;
@@ -62,9 +59,6 @@
@Slf4j
public class PresenceCheckService {
- private static final String SESSION_ATTRIBUTE_TIMESTAMP_LAST_USED = "timestampLastUsed";
- private static final String SESSION_ATTRIBUTE_IMAGE_UPLOADED = "imageUploaded";
-
private final IdentityVerificationConfig identityVerificationConfig;
private final DocumentVerificationRepository documentVerificationRepository;
private final DocumentProcessingService documentProcessingService;
@@ -128,7 +122,7 @@ public void checkPresenceVerification(
final OwnerId ownerId,
final IdentityVerificationEntity idVerification) throws PresenceCheckException, RemoteCommunicationException {
- final SessionInfo sessionInfo = updateSessionInfo(ownerId, idVerification, Map.of(SESSION_ATTRIBUTE_TIMESTAMP_LAST_USED, ownerId.getTimestamp()));
+ final SessionInfo sessionInfo = updateSessionInfo(ownerId, idVerification, Map.of(SessionInfo.ATTRIBUTE_TIMESTAMP_LAST_USED, ownerId.getTimestamp()));
final PresenceCheckResult result = presenceCheckProvider.getResult(ownerId, sessionInfo);
auditService.auditPresenceCheckProvider(idVerification, "Got presence check result: {} for user: {}", result.getStatus(), ownerId.getUserId());
@@ -216,15 +210,16 @@ private void initPresentCheckWithImage(final OwnerId ownerId, final IdentityVeri
}
final Optional photo = fetchTrustedPhoto(ownerId, idVerification);
+ setIdentityDocumentReferences(ownerId, idVerification);
presenceCheckProvider.initPresenceCheck(ownerId, photo.orElse(null));
logger.info("Presence check initialized, {}", ownerId);
- updateSessionInfo(ownerId, idVerification, Map.of(SESSION_ATTRIBUTE_IMAGE_UPLOADED, true));
+ updateSessionInfo(ownerId, idVerification, Map.of(SessionInfo.ATTRIBUTE_IMAGE_UPLOADED, true));
auditService.auditPresenceCheckProvider(idVerification, "Presence check initialized for user: {}", ownerId.getUserId());
}
}
private Optional fetchTrustedPhoto(final OwnerId ownerId, final IdentityVerificationEntity idVerification) throws DocumentVerificationException, RemoteCommunicationException, PresenceCheckException {
- if (presenceCheckProvider.shouldProvideTrustedPhoto()) {
+ if (PresenceCheckProvider.TrustedPhotoSource.IMAGE == presenceCheckProvider.trustedPhotoSource()) {
final Image photo = fetchTrustedPhotoFromDocumentVerifier(ownerId, idVerification);
final Image upscaledPhoto = imageProcessor.upscaleImage(ownerId, photo, identityVerificationConfig.getMinimalSelfieWidth());
return Optional.of(upscaledPhoto);
@@ -233,6 +228,22 @@ private Optional fetchTrustedPhoto(final OwnerId ownerId, final IdentityV
}
}
+ private void setIdentityDocumentReferences(final OwnerId ownerId, final IdentityVerificationEntity idVerification) throws DocumentVerificationException, PresenceCheckException {
+ if (PresenceCheckProvider.TrustedPhotoSource.REFERENCE != presenceCheckProvider.trustedPhotoSource()) {
+ return;
+ }
+
+ final List docsWithPhoto = getDocsWithPhoto(idVerification, ownerId);
+ final String primaryDocReference = getPreferredDocWithPhoto(docsWithPhoto, ownerId).getPhotoId();
+ final List otherDocsReferences = docsWithPhoto.stream()
+ .map(DocumentVerificationEntity::getPhotoId)
+ .filter(id -> !Objects.equals(id, primaryDocReference))
+ .distinct().toList();
+
+ updateSessionInfo(ownerId, idVerification, Map.of(SessionInfo.ATTRIBUTE_PRIMARY_DOCUMENT_REFERENCE, primaryDocReference,
+ SessionInfo.ATTRIBUTE_OTHER_DOCUMENTS_REFERENCES, otherDocsReferences));
+ }
+
/**
* Starts new presence check process.
*
@@ -264,6 +275,15 @@ private SessionInfo startPresenceCheck(OwnerId ownerId, IdentityVerificationEnti
protected Image fetchTrustedPhotoFromDocumentVerifier(final OwnerId ownerId, final IdentityVerificationEntity idVerification)
throws DocumentVerificationException, RemoteCommunicationException {
+ final List docsWithPhoto = getDocsWithPhoto(idVerification, ownerId);
+ final DocumentVerificationEntity preferredDocWithPhoto = getPreferredDocWithPhoto(docsWithPhoto, ownerId);
+ logger.info("Selected {} as the source of person photo, {}", preferredDocWithPhoto, ownerId);
+ final String photoId = preferredDocWithPhoto.getPhotoId();
+ return identityVerificationService.getPhotoById(photoId, ownerId);
+ }
+
+ private List getDocsWithPhoto(final IdentityVerificationEntity idVerification,
+ final OwnerId ownerId) throws DocumentVerificationException {
final List docsWithPhoto = documentVerificationRepository.findAllWithPhoto(idVerification);
if (docsWithPhoto.isEmpty()) {
throw new DocumentVerificationException("Unable to initialize presence check - missing person photo, " + ownerId);
@@ -273,23 +293,23 @@ protected Image fetchTrustedPhotoFromDocumentVerifier(final OwnerId ownerId, fin
Preconditions.checkNotNull(docWithPhoto.getPhotoId(), "Expected photoId value in " + docWithPhoto)
);
- DocumentVerificationEntity preferredDocWithPhoto = null;
+ return docsWithPhoto;
+ }
+
+ private DocumentVerificationEntity getPreferredDocWithPhoto(final List docsWithPhoto,
+ final OwnerId ownerId) {
+
for (DocumentType documentType : DocumentType.PREFERRED_SOURCE_OF_PERSON_PHOTO) {
Optional docEntity = docsWithPhoto.stream()
.filter(value -> value.getType() == documentType)
.findFirst();
if (docEntity.isPresent()) {
- preferredDocWithPhoto = docEntity.get();
- break;
+ return docEntity.get();
}
}
- if (preferredDocWithPhoto == null) {
- logger.warn("Unable to select a source of person photo to initialize presence check, {}", ownerId);
- preferredDocWithPhoto = docsWithPhoto.get(0);
- }
- logger.info("Selected {} as the source of person photo, {}", preferredDocWithPhoto, ownerId);
- String photoId = preferredDocWithPhoto.getPhotoId();
- return identityVerificationService.getPhotoById(photoId, ownerId);
+
+ logger.warn("Unable to select a source of person photo to initialize presence check, {}", ownerId);
+ return docsWithPhoto.get(0);
}
private void evaluatePresenceCheckResult(OwnerId ownerId,
@@ -389,6 +409,6 @@ private boolean imageAlreadyUploaded(final IdentityVerificationEntity identityVe
final SessionInfo sessionInfo = jsonSerializationService.deserialize(sessionInfoString, SessionInfo.class);
return sessionInfo != null
&& !CollectionUtils.isEmpty(sessionInfo.getSessionAttributes())
- && Boolean.TRUE.equals(sessionInfo.getSessionAttributes().get(SESSION_ATTRIBUTE_IMAGE_UPLOADED));
+ && Boolean.TRUE.equals(sessionInfo.getSessionAttributes().get(SessionInfo.ATTRIBUTE_IMAGE_UPLOADED));
}
}
diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingService.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingService.java
index 948919729..7dae9452b 100644
--- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingService.java
+++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingService.java
@@ -17,6 +17,7 @@
*/
package com.wultra.app.onboardingserver.impl.service.document;
+import com.google.common.base.Strings;
import com.wultra.app.enrollmentserver.api.model.onboarding.request.DocumentSubmitRequest;
import com.wultra.app.enrollmentserver.model.Document;
import com.wultra.app.enrollmentserver.model.DocumentMetadata;
@@ -43,10 +44,9 @@
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Optional;
+import java.util.*;
+
+import static java.util.stream.Collectors.groupingBy;
/**
* Service implementing document processing features.
@@ -119,22 +119,44 @@ public List submitDocuments(
DocumentSubmitRequest request,
OwnerId ownerId) throws DocumentSubmitException {
- List documents = getDocuments(ownerId, request);
-
- List docVerifications = new ArrayList<>();
- List docResults = new ArrayList<>();
+ checkDocumentResubmit(ownerId, request);
+ final List documents = getDocuments(ownerId, request);
+ final var documentsByType = request.getDocuments().stream()
+ .collect(groupingBy(DocumentSubmitRequest.DocumentMetadata::getType));
- List docsMetadata = request.getDocuments();
- for (DocumentSubmitRequest.DocumentMetadata docMetadata : docsMetadata) {
- DocumentVerificationEntity docVerification = createDocumentVerification(ownerId, idVerification, docMetadata);
- docVerification.setIdentityVerification(idVerification);
- docVerifications.add(docVerification);
+ final List docVerifications = new ArrayList<>();
+ for (var docMetadataList : documentsByType.values()) {
+ docVerifications.addAll(submitDocument(docMetadataList, documents, idVerification, ownerId));
+ }
+ return docVerifications;
+ }
- checkDocumentResubmit(ownerId, request, docVerification);
+ /**
+ * Submit pages of a document to document verify provider.
+ * @param pagesMetadata Pages metadata from request.
+ * @param pagesData Pages data.
+ * @param idVerification Identity verification entity.
+ * @param ownerId Owner identification.
+ * @return
+ */
+ private List submitDocument(final List pagesMetadata,
+ final List pagesData,
+ final IdentityVerificationEntity idVerification,
+ final OwnerId ownerId) {
+
+ // Maps are used to associate DocumentsSubmitResult - DocumentVerificationEntities - DocumentMetadata
+ final Map docVerifications = new HashMap<>();
+ final Map docMetadataMap = new HashMap<>();
+
+ final List submittedDocuments = new ArrayList<>();
+ for (var metadata : pagesMetadata) {
+ final DocumentVerificationEntity docVerification = createDocumentVerification(ownerId, idVerification, metadata);
+ docVerifications.put(docVerification.getId(), docVerification);
+ docMetadataMap.put(docVerification.getId(), metadata);
+ handleResubmit(ownerId, metadata.getOriginalDocumentId(), docVerification);
- SubmittedDocument submittedDoc;
try {
- submittedDoc = createSubmittedDocument(ownerId, docMetadata, documents);
+ submittedDocuments.add(createSubmittedDocument(ownerId, metadata, pagesData, docVerification));
} catch (DocumentSubmitException e) {
logger.warn("Document verification ID: {}, failed: {}", docVerification.getId(), e.getMessage());
logger.debug("Document verification ID: {}, failed", docVerification.getId(), e);
@@ -142,60 +164,98 @@ public List submitDocuments(
docVerification.setErrorDetail(ErrorDetail.DOCUMENT_VERIFICATION_FAILED);
docVerification.setErrorOrigin(ErrorOrigin.DOCUMENT_VERIFICATION);
auditService.audit(docVerification, "Document verification failed for user: {}", ownerId.getUserId());
- return docVerifications;
+ return docVerifications.values().stream().toList();
}
+ }
- DocumentSubmitResult docSubmitResult = submitDocumentToProvider(ownerId, docVerification, submittedDoc);
+ final List docVerificationsList = docVerifications.values().stream().toList();
+ final DocumentsSubmitResult results = submitDocumentToProvider(submittedDocuments, docVerificationsList, idVerification, ownerId);
+ processSubmitResults(results, docVerifications, ownerId);
- // TODO - after synchronous submission to document verification provider the document state should be
- // set to VERIFICATION_PENDING, for asynchronous processing the UPLOAD_IN_PROGRESS state should remain
+ docVerificationsList.stream()
+ .filter(doc -> StringUtils.isNotBlank(doc.getUploadId()))
+ .map(doc -> docMetadataMap.get(doc.getId()).getUploadId())
+ .filter(StringUtils::isNotBlank)
+ .forEach(fileUploadId -> {
+ documentDataRepository.deleteById(fileUploadId);
+ logger.info("Deleted stored document data with id={}, {}", fileUploadId, ownerId);
+ });
- DocumentResultEntity docResult = createDocumentResult(docVerification, docSubmitResult);
- docResult.setTimestampCreated(ownerId.getTimestamp());
+ return docVerificationsList;
+ }
- docResults.add(docResult);
+ /**
+ * Process submit results.
+ * @param results Document submit result from provider.
+ * @param docVerificationsMap To pair results with corresponding DocumentVerificationEntity.
+ * @param ownerId Owner identification.
+ */
+ private void processSubmitResults(final DocumentsSubmitResult results,
+ final Map docVerificationsMap,
+ final OwnerId ownerId) {
- // Delete previously persisted document after a successful upload to the provider
- if (docVerification.getUploadId() != null && docMetadata.getUploadId() != null) {
- documentDataRepository.deleteById(docMetadata.getUploadId());
- }
- }
+ final List docResults = new ArrayList<>();
- documentVerificationRepository.saveAll(docVerifications);
+ for (final DocumentSubmitResult result : results.getResults()) {
+ final DocumentVerificationEntity docVerification = docVerificationsMap.get(result.getDocumentId());
+ processDocsSubmitResults(ownerId, docVerification, results, result);
- for (int i = 0; i < docVerifications.size(); i++) {
- DocumentVerificationEntity docVerificationEntity = docVerifications.get(i);
- docResults.get(i).setDocumentVerification(docVerificationEntity);
+ final DocumentResultEntity docResult = createDocumentResult(docVerification, result);
+ docResult.setTimestampCreated(ownerId.getTimestamp());
+ docResult.setDocumentVerification(docVerification);
+
+ docResults.add(docResult);
}
- documentResultRepository.saveAll(docResults);
- return docVerifications;
+ documentVerificationRepository.saveAll(docVerificationsMap.values());
+ documentResultRepository.saveAll(docResults);
+ logger.debug("Processed submit result of documents {}, {}", docVerificationsMap.values(), ownerId);
}
- public void checkDocumentResubmit(OwnerId ownerId,
- DocumentSubmitRequest request,
- DocumentVerificationEntity docVerification) throws DocumentSubmitException {
- if (request.isResubmit() && docVerification.getOriginalDocumentId() == null) {
- throw new DocumentSubmitException(
- String.format("Detected a resubmit request without specified originalDocumentId for %s, %s", docVerification, ownerId));
- } else if (request.isResubmit()) {
- Optional originalDocOptional =
- documentVerificationRepository.findById(docVerification.getOriginalDocumentId());
- if (originalDocOptional.isEmpty()) {
- logger.warn("Missing previous DocumentVerificationEntity(originalDocumentId={}), {}",
- docVerification.getOriginalDocumentId(), ownerId);
- } else {
- DocumentVerificationEntity originalDoc = originalDocOptional.get();
- originalDoc.setStatus(DocumentStatus.DISPOSED);
- originalDoc.setUsedForVerification(false);
- originalDoc.setTimestampDisposed(ownerId.getTimestamp());
- originalDoc.setTimestampLastUpdated(ownerId.getTimestamp());
- logger.info("Replaced previous {} with new {}, {}", originalDocOptional, docVerification, ownerId);
- auditService.audit(docVerification, "Document replaced with new one for user: {}", ownerId.getUserId());
+ /**
+ * Validates resubmit parameters of DocumentSubmitRequest.
+ * @param ownerId Owner identification.
+ * @param request Request body.
+ * @throws DocumentSubmitException If request is resubmit without original document ID, or is not resubmit with original document ID
+ */
+ private void checkDocumentResubmit(final OwnerId ownerId, final DocumentSubmitRequest request) throws DocumentSubmitException {
+ final boolean isResubmit = request.isResubmit();
+ for (var metadata : request.getDocuments()) {
+ final String originalDocumentId = metadata.getOriginalDocumentId();
+ if (isResubmit && StringUtils.isBlank(originalDocumentId)) {
+ logger.debug("Request has resubmit flag but misses originalDocumentId {}, {}", metadata, ownerId);
+ throw new DocumentSubmitException("Detected a resubmit request without specified originalDocumentId, %s".formatted(ownerId));
+ } else if (!isResubmit && StringUtils.isNotBlank(originalDocumentId)) {
+ logger.debug("Request has originalDocumentId but is not flagged as resubmit {}, {}", metadata, ownerId);
+ throw new DocumentSubmitException("Detected a submit request with specified originalDocumentId=%s, %s".formatted(originalDocumentId, ownerId));
}
- } else if (!request.isResubmit() && docVerification.getOriginalDocumentId() != null) {
- throw new DocumentSubmitException(
- String.format("Detected a submit request with specified originalDocumentId=%s for %s, %s", docVerification.getOriginalDocumentId(), docVerification, ownerId));
+ }
+ }
+
+ /**
+ * Sets document with originalDocumentId as disposed. If passed originalDocumentId does not exist, no further action is taken.
+ * @param ownerId Owner identification.
+ * @param originalDocumentId Id of the original document.
+ * @param docVerification Resubmitted document.
+ */
+ private void handleResubmit(final OwnerId ownerId, final String originalDocumentId, final DocumentVerificationEntity docVerification) {
+ if (Strings.isNullOrEmpty(originalDocumentId)) {
+ logger.debug("Document {} is not a resubmit {}", docVerification, ownerId);
+ return;
+ }
+
+ logger.debug("Document {} is a resubmit, {}", docVerification, ownerId);
+ final Optional originalDocOptional = documentVerificationRepository.findById(originalDocumentId);
+ if (originalDocOptional.isEmpty()) {
+ logger.warn("Missing previous DocumentVerificationEntity(originalDocumentId={}), {}", originalDocumentId, ownerId);
+ } else {
+ final DocumentVerificationEntity originalDoc = originalDocOptional.get();
+ originalDoc.setStatus(DocumentStatus.DISPOSED);
+ originalDoc.setUsedForVerification(false);
+ originalDoc.setTimestampDisposed(ownerId.getTimestamp());
+ originalDoc.setTimestampLastUpdated(ownerId.getTimestamp());
+ logger.info("Replaced previous {} with new {}, {}", originalDoc, docVerification, ownerId);
+ auditService.audit(docVerification, "Document replaced with new one for user: {}", ownerId.getUserId());
}
}
@@ -243,6 +303,41 @@ public void checkDocumentSubmitWithProvider(OwnerId ownerId, DocumentResultEntit
processDocsSubmitResults(ownerId, docVerification, docsSubmitResults, docSubmitResult);
}
+ /**
+ * Pass all pages of a document to document verification provider at a single call.
+ * @param submittedDocs Document pages to submit.
+ * @param docVerifications Entities associated with the document pages to submit.
+ * @param identityVerification Identity verification entity.
+ * @param ownerId Owner identification.
+ * @return document submit result
+ */
+ private DocumentsSubmitResult submitDocumentToProvider(final List submittedDocs,
+ final List docVerifications,
+ final IdentityVerificationEntity identityVerification,
+ final OwnerId ownerId) {
+
+ final List docVerificationIds = docVerifications.stream().map(DocumentVerificationEntity::getId).toList();
+
+ try {
+ final DocumentsSubmitResult results = documentVerificationProvider.submitDocuments(ownerId, submittedDocs);
+ logger.debug("Documents {} submitted to provider, {}", docVerifications, ownerId);
+ auditService.auditDocumentVerificationProvider(identityVerification, "Submit documents for user: {}, document IDs: {}", ownerId.getUserId(), docVerificationIds);
+ return results;
+ } catch (DocumentVerificationException | RemoteCommunicationException e) {
+ logger.warn("Document verification ID: {}, failed: {}", docVerificationIds, e.getMessage());
+ logger.debug("Document verification ID: {}, failed", docVerificationIds, e);
+ final DocumentsSubmitResult results = new DocumentsSubmitResult();
+ submittedDocs.forEach(doc -> {
+ final DocumentSubmitResult result = new DocumentSubmitResult();
+ result.setDocumentId(doc.getDocumentId());
+ result.setErrorDetail(e.getMessage());
+ results.getResults().add(result);
+ });
+ return results;
+ }
+
+ }
+
public DocumentSubmitResult submitDocumentToProvider(OwnerId ownerId, DocumentVerificationEntity docVerification, SubmittedDocument submittedDoc) {
DocumentsSubmitResult docsSubmitResults;
DocumentSubmitResult docSubmitResult;
@@ -293,8 +388,8 @@ public void pairTwoSidedDocuments(List documents) {
continue;
}
documents.stream()
- .filter(item -> item.getType().equals(document.getType()))
- .filter(item -> !item.getSide().equals(document.getSide()))
+ .filter(item -> item.getType() == document.getType())
+ .filter(item -> item.getSide() != document.getSide())
.forEach(item -> {
logger.debug("Found other side {} for {}", item, document);
item.setOtherSideId(document.getId());
@@ -353,6 +448,7 @@ private DocumentVerificationEntity createDocumentVerification(OwnerId ownerId, I
entity.setTimestampCreated(ownerId.getTimestamp());
entity.setUsedForVerification(true);
final DocumentVerificationEntity saveEntity = documentVerificationRepository.save(entity);
+ logger.debug("Created {} for {}", saveEntity, ownerId);
auditService.auditDebug(entity, "Document created for user: {}", ownerId.getUserId());
return saveEntity;
}
@@ -360,13 +456,14 @@ private DocumentVerificationEntity createDocumentVerification(OwnerId ownerId, I
private SubmittedDocument createSubmittedDocument(
OwnerId ownerId,
DocumentSubmitRequest.DocumentMetadata docMetadata,
- List docs) throws DocumentSubmitException {
+ List docs,
+ DocumentVerificationEntity docVerification) throws DocumentSubmitException {
final Image photo = Image.builder()
.filename(docMetadata.getFilename())
.build();
SubmittedDocument submittedDoc = new SubmittedDocument();
- submittedDoc.setDocumentId(docMetadata.getUploadId());
+ submittedDoc.setDocumentId(docVerification.getId());
submittedDoc.setPhoto(photo);
submittedDoc.setSide(docMetadata.getSide());
submittedDoc.setType(docMetadata.getType());
diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/presencecheck/mock/provider/WultraMockPresenceCheckProvider.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/presencecheck/mock/provider/WultraMockPresenceCheckProvider.java
index a42241794..4be8ac2f5 100644
--- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/presencecheck/mock/provider/WultraMockPresenceCheckProvider.java
+++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/presencecheck/mock/provider/WultraMockPresenceCheckProvider.java
@@ -62,8 +62,8 @@ public void initPresenceCheck(OwnerId id, Image photo) {
}
@Override
- public boolean shouldProvideTrustedPhoto() {
- return true;
+ public TrustedPhotoSource trustedPhotoSource() {
+ return TrustedPhotoSource.IMAGE;
}
@Override
diff --git a/enrollment-server-onboarding/src/main/resources/application.properties b/enrollment-server-onboarding/src/main/resources/application.properties
index a21dcc03f..9b608a5d4 100644
--- a/enrollment-server-onboarding/src/main/resources/application.properties
+++ b/enrollment-server-onboarding/src/main/resources/application.properties
@@ -173,7 +173,11 @@ enrollment-server-onboarding.provider.innovatrics.serviceToken=${INNOVATRICS_SER
enrollment-server-onboarding.provider.innovatrics.serviceUserAgent=Wultra/OnboardingServer
# Innovatrics presence-check configuration
-enrollment-server-onboarding.provider.innovatrics.presenceCheck.score=0.875
+enrollment-server-onboarding.provider.innovatrics.presenceCheckConfiguration.score=0.875
+
+# Innovatrics document-verification configuration
+enrollment-server-onboarding.provider.innovatrics.documentVerificationConfiguration.documentCountries=CZE
+enrollment-server-onboarding.provider.innovatrics.document-verification-configuration.crucialFields=documentNumber, dateOfIssue, dateOfExpiry, surname, dateOfBirth, personalNumber, givenNames
# Innovatrics REST client configuration
enrollment-server-onboarding.provider.innovatrics.restClientConfig.acceptInvalidSslCertificate=false
diff --git a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/docverify/mock/provider/WultraMockDocumentVerificationProviderTest.java b/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/docverify/mock/provider/WultraMockDocumentVerificationProviderTest.java
index 721d72e80..bad60c5bd 100644
--- a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/docverify/mock/provider/WultraMockDocumentVerificationProviderTest.java
+++ b/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/docverify/mock/provider/WultraMockDocumentVerificationProviderTest.java
@@ -65,7 +65,7 @@ public void setProvider(WultraMockDocumentVerificationProvider provider) {
}
@Test
- void checkDocumentUploadTest() {
+ void checkDocumentUploadTest() throws Exception {
SubmittedDocument document = createSubmittedDocument();
DocumentsSubmitResult submitResult = provider.submitDocuments(ownerId, List.of(document));
diff --git a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckServiceTest.java b/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckServiceTest.java
index c5daedb85..c256c6d5d 100644
--- a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckServiceTest.java
+++ b/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckServiceTest.java
@@ -16,20 +16,39 @@
*/
package com.wultra.app.onboardingserver.impl.service;
+import com.wultra.app.enrollmentserver.model.enumeration.CardSide;
import com.wultra.app.enrollmentserver.model.enumeration.DocumentType;
+import com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationPhase;
+import com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationStatus;
import com.wultra.app.enrollmentserver.model.integration.Image;
import com.wultra.app.enrollmentserver.model.integration.OwnerId;
+import com.wultra.app.enrollmentserver.model.integration.SessionInfo;
+import com.wultra.app.onboardingserver.EnrollmentServerTestApplication;
+import com.wultra.app.onboardingserver.api.provider.PresenceCheckProvider;
import com.wultra.app.onboardingserver.common.database.DocumentVerificationRepository;
+import com.wultra.app.onboardingserver.common.database.IdentityVerificationRepository;
+import com.wultra.app.onboardingserver.common.database.ScaResultRepository;
import com.wultra.app.onboardingserver.common.database.entity.DocumentVerificationEntity;
import com.wultra.app.onboardingserver.common.database.entity.IdentityVerificationEntity;
+import com.wultra.app.onboardingserver.impl.service.internal.JsonSerializationService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.ActiveProfiles;
import java.util.List;
+import java.util.Optional;
+import static com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationPhase.DOCUMENT_VERIFICATION_FINAL;
+import static com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationPhase.PRESENCE_CHECK;
+import static com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationStatus.*;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.*;
/**
@@ -38,16 +57,23 @@
* @author Lukas Lukovsky, lukas.lukovsky@wultra.com
* @author Lubos Racansky, lubos.racansky@wultra.com
*/
-@ExtendWith(MockitoExtension.class)
+@SpringBootTest(classes = EnrollmentServerTestApplication.class)
+@ActiveProfiles("test")
class PresenceCheckServiceTest {
- @Mock
+ @MockBean
private IdentityVerificationService identityVerificationService;
- @Mock
+ @MockBean
private DocumentVerificationRepository documentVerificationRepository;
- @InjectMocks
+ @MockBean
+ private PresenceCheckLimitService presenceCheckLimitService;
+
+ @MockBean
+ private PresenceCheckProvider presenceCheckProvider;
+
+ @Autowired
private PresenceCheckService tested;
@Test
@@ -94,4 +120,44 @@ void testFetchTrustedPhotoFromDocumentVerifier_unknownDocument() throws Exceptio
verify(identityVerificationService, times(1)).getPhotoById(docPhotoUnknown.getPhotoId(), ownerId);
}
+ @Test
+ void initPresentCheckWithImage_withDocumentReferences() throws Exception {
+ final OwnerId ownerId = new OwnerId();
+ ownerId.setActivationId("a1");
+
+ final DocumentVerificationEntity page1 = new DocumentVerificationEntity();
+ page1.setId("1");
+ page1.setType(DocumentType.ID_CARD);
+ page1.setSide(CardSide.FRONT);
+ page1.setPhotoId("id_card_portrait");
+
+ final DocumentVerificationEntity page2 = new DocumentVerificationEntity();
+ page2.setId("2");
+ page2.setType(DocumentType.ID_CARD);
+ page2.setSide(CardSide.BACK);
+ page2.setPhotoId("id_card_portrait");
+
+ final DocumentVerificationEntity page3 = new DocumentVerificationEntity();
+ page3.setId("3");
+ page3.setType(DocumentType.DRIVING_LICENSE);
+ page3.setSide(CardSide.FRONT);
+ page3.setPhotoId("driving_licence_portrait");
+
+ when(presenceCheckProvider.trustedPhotoSource()).thenReturn(PresenceCheckProvider.TrustedPhotoSource.REFERENCE);
+
+ final IdentityVerificationEntity identityVerification = new IdentityVerificationEntity();
+ identityVerification.setPhase(PRESENCE_CHECK);
+ identityVerification.setStatus(NOT_INITIALIZED);
+
+ when(documentVerificationRepository.findAllWithPhoto(identityVerification)).thenReturn(List.of(page1, page2, page3));
+ when(identityVerificationService.findByOptional(ownerId)).thenReturn(Optional.of(identityVerification));
+ when(presenceCheckProvider.startPresenceCheck(ownerId)).thenReturn(new SessionInfo());
+
+ tested.init(ownerId, "p1");
+
+ assertTrue(identityVerification.getSessionInfo().contains("\"primaryDocumentReference\":\"id_card_portrait\""));
+ assertTrue(identityVerification.getSessionInfo().contains("\"otherDocumentsReferences\":[\"driving_licence_portrait\"]"));
+ verify(presenceCheckProvider).initPresenceCheck(ownerId, null);
+ }
+
}
diff --git a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingServiceTest.java b/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingServiceTest.java
index 92fd2aebf..37309ec05 100644
--- a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingServiceTest.java
+++ b/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingServiceTest.java
@@ -17,58 +17,274 @@
*/
package com.wultra.app.onboardingserver.impl.service.document;
+import com.wultra.app.enrollmentserver.api.model.onboarding.request.DocumentSubmitRequest;
+import com.wultra.app.enrollmentserver.model.Document;
+import com.wultra.app.enrollmentserver.model.enumeration.*;
+import com.wultra.app.enrollmentserver.model.integration.*;
+import com.wultra.app.onboardingserver.EnrollmentServerTestApplication;
+import com.wultra.app.onboardingserver.common.database.DocumentResultRepository;
import com.wultra.app.onboardingserver.common.database.DocumentVerificationRepository;
+import com.wultra.app.onboardingserver.common.database.IdentityVerificationRepository;
+import com.wultra.app.onboardingserver.common.database.entity.DocumentResultEntity;
import com.wultra.app.onboardingserver.common.database.entity.DocumentVerificationEntity;
-import com.wultra.app.enrollmentserver.model.enumeration.CardSide;
-import com.wultra.app.enrollmentserver.model.enumeration.DocumentType;
-import org.junit.jupiter.api.BeforeEach;
+import com.wultra.app.onboardingserver.common.database.entity.IdentityVerificationEntity;
+import com.wultra.app.onboardingserver.errorhandling.DocumentSubmitException;
+import com.wultra.app.onboardingserver.impl.service.DataExtractionService;
import org.junit.jupiter.api.Test;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.jdbc.Sql;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
-import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
/**
* @author Lukas Lukovsky, lukas.lukovsky@wultra.com
+ * @author Jan Pesek, jan.pesek@wultra.com
*/
+@SpringBootTest(classes = EnrollmentServerTestApplication.class)
+@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
+@ActiveProfiles("test")
+@Sql
class DocumentProcessingServiceTest {
- @InjectMocks
- DocumentProcessingService service;
+ @MockBean
+ DataExtractionService dataExtractionService;
- @Mock
+ @Autowired
+ DocumentProcessingService tested;
+
+ @Autowired
DocumentVerificationRepository documentVerificationRepository;
- @BeforeEach
- public void init() {
- MockitoAnnotations.openMocks(this);
+ @Autowired
+ IdentityVerificationRepository identityVerificationRepository;
+
+ @Autowired
+ DocumentResultRepository documentResultRepository;
+
+ @Test
+ @Sql
+ void testPairTwoSidedDocuments() {
+ tested.pairTwoSidedDocuments(documentVerificationRepository.findAll());
+ assertEquals("2", documentVerificationRepository.findById("1").map(DocumentVerificationEntity::getOtherSideId).get());
+ assertEquals("1", documentVerificationRepository.findById("2").map(DocumentVerificationEntity::getOtherSideId).get());
+ }
+
+ @Test
+ void testSubmitDocuments() throws Exception {
+ final IdentityVerificationEntity identityVerification = identityVerificationRepository.findById("v1").get();
+ assertNotNull(identityVerification);
+
+ final List metadata = createIdCardMetadata();
+ final List data = createIdCardData();
+ final OwnerId ownerId = createOwnerId();
+
+ final DocumentSubmitRequest request = new DocumentSubmitRequest();
+ request.setProcessId("p1");
+ request.setResubmit(false);
+ request.setData("files".getBytes());
+ request.setDocuments(metadata);
+ when(dataExtractionService.extractDocuments(request.getData())).thenReturn(data);
+
+ tested.submitDocuments(identityVerification, request, ownerId);
+
+ final List documents = documentVerificationRepository.findAll();
+ assertEquals(2, documents.size());
+ assertThat(documents)
+ .extracting(DocumentVerificationEntity::getSide)
+ .containsExactlyInAnyOrder(CardSide.FRONT, CardSide.BACK);
+ assertThat(documents)
+ .extracting(DocumentVerificationEntity::getStatus)
+ .containsOnly(DocumentStatus.VERIFICATION_PENDING);
+
+ final List results = new ArrayList<>();
+ documentResultRepository.findAll().forEach(results::add);
+ assertEquals(2, results.size());
+ assertThat(results)
+ .extracting(DocumentResultEntity::getDocumentVerification)
+ .extracting(DocumentVerificationEntity::getId)
+ .containsExactlyInAnyOrder(documents.stream().map(DocumentVerificationEntity::getId).toArray(String[]::new));
+ assertThat(results)
+ .extracting(DocumentResultEntity::getPhase)
+ .containsOnly(DocumentProcessingPhase.UPLOAD);
+ }
+
+ @Test
+ void testSubmitDocuments_providerThrows() throws Exception {
+ final IdentityVerificationEntity identityVerification = identityVerificationRepository.findById("v1").get();
+ assertNotNull(identityVerification);
+
+ final List metadata = createIdCardMetadata();
+ metadata.get(1).setFilename("throw.exception");
+ final List data = createIdCardData();
+ data.get(1).setFilename("throw.exception");
+ final OwnerId ownerId = createOwnerId();
+
+ final DocumentSubmitRequest request = new DocumentSubmitRequest();
+ request.setProcessId("p1");
+ request.setResubmit(false);
+ request.setData("files".getBytes());
+ request.setDocuments(metadata);
+ when(dataExtractionService.extractDocuments(request.getData())).thenReturn(data);
+
+ tested.submitDocuments(identityVerification, request, ownerId);
+
+ final List documents = documentVerificationRepository.findAll();
+ assertEquals(2, documents.size());
+ assertThat(documents)
+ .extracting(DocumentVerificationEntity::getSide)
+ .containsExactlyInAnyOrder(CardSide.FRONT, CardSide.BACK);
+ assertThat(documents)
+ .extracting(DocumentVerificationEntity::getStatus)
+ .containsOnly(DocumentStatus.FAILED);
+
+ final List results = new ArrayList<>();
+ documentResultRepository.findAll().forEach(results::add);
+ assertEquals(2, results.size());
+ assertThat(results)
+ .extracting(DocumentResultEntity::getErrorDetail)
+ .containsOnly("documentVerificationFailed");
+ assertThat(results)
+ .extracting(DocumentResultEntity::getErrorOrigin)
+ .containsOnly(ErrorOrigin.DOCUMENT_VERIFICATION);
+ }
+
+ @Test
+ void testSubmitDocuments_missingData() throws Exception {
+ final IdentityVerificationEntity identityVerification = identityVerificationRepository.findById("v1").get();
+ assertNotNull(identityVerification);
+
+ final List metadata = createIdCardMetadata();
+ final List data = Collections.emptyList();
+ final OwnerId ownerId = createOwnerId();
+
+ final DocumentSubmitRequest request = new DocumentSubmitRequest();
+ request.setProcessId("p1");
+ request.setResubmit(false);
+ request.setData("files".getBytes());
+ request.setDocuments(metadata);
+ when(dataExtractionService.extractDocuments(request.getData())).thenReturn(data);
+
+ tested.submitDocuments(identityVerification, request, ownerId);
+
+ List documents = documentVerificationRepository.findAll();
+ assertEquals(1, documents.size());
+ assertThat(documents)
+ .extracting(DocumentVerificationEntity::getStatus)
+ .containsExactlyInAnyOrder(DocumentStatus.FAILED);
+ }
+
+ @Test
+ @Sql
+ void testResubmitDocuments() throws Exception {
+ final IdentityVerificationEntity identityVerification = identityVerificationRepository.findById("v1").get();
+ assertNotNull(identityVerification);
+
+ final List metadata = createIdCardMetadata();
+ metadata.get(0).setOriginalDocumentId("original1");
+ metadata.get(1).setOriginalDocumentId("original2");
+ final List data = createIdCardData();
+ final OwnerId ownerId = createOwnerId();
+
+ final DocumentSubmitRequest request = new DocumentSubmitRequest();
+ request.setProcessId("p1");
+ request.setResubmit(true);
+ request.setData("files".getBytes());
+ request.setDocuments(metadata);
+ when(dataExtractionService.extractDocuments(request.getData())).thenReturn(data);
+
+ tested.submitDocuments(identityVerification, request, ownerId);
+ List documents = documentVerificationRepository.findAll();
+ assertEquals(4, documents.size());
+ assertThat(documents)
+ .extracting(DocumentVerificationEntity::getStatus)
+ .containsOnly(DocumentStatus.VERIFICATION_PENDING, DocumentStatus.DISPOSED);
+ }
+
+ @Test
+ void testResubmitDocuments_missingOriginalDocumentId() throws Exception {
+ final IdentityVerificationEntity identityVerification = identityVerificationRepository.findById("v1").get();
+ assertNotNull(identityVerification);
+
+ final List metadata = createIdCardMetadata();
+ final List data = createIdCardData();
+ final OwnerId ownerId = createOwnerId();
+
+ final DocumentSubmitRequest request = new DocumentSubmitRequest();
+ request.setProcessId("p1");
+ request.setResubmit(true);
+ request.setData("files".getBytes());
+ request.setDocuments(metadata);
+ when(dataExtractionService.extractDocuments(request.getData())).thenReturn(data);
+
+ final DocumentSubmitException exception = assertThrows(DocumentSubmitException.class,
+ () -> tested.submitDocuments(identityVerification, request, ownerId));
+ assertEquals("Detected a resubmit request without specified originalDocumentId, %s".formatted(ownerId), exception.getMessage());
}
@Test
- void pairTwoSidedDocumentsTest() {
- DocumentVerificationEntity docIdCardFront = new DocumentVerificationEntity();
- docIdCardFront.setId("1");
- docIdCardFront.setType(DocumentType.ID_CARD);
- docIdCardFront.setSide(CardSide.FRONT);
-
- DocumentVerificationEntity docIdCardBack = new DocumentVerificationEntity();
- docIdCardBack.setId("2");
- docIdCardBack.setType(DocumentType.ID_CARD);
- docIdCardBack.setSide(CardSide.BACK);
-
- List documents = List.of(docIdCardFront, docIdCardBack);
-
- service.pairTwoSidedDocuments(documents);
- when(documentVerificationRepository.setOtherDocumentSide("1", "2")).thenReturn(1);
- verify(documentVerificationRepository, times(1)).setOtherDocumentSide("1", "2");
- when(documentVerificationRepository.setOtherDocumentSide("2", "1")).thenReturn(1);
- verify(documentVerificationRepository, times(1)).setOtherDocumentSide("2", "1");
- assertEquals(docIdCardBack.getId(), docIdCardFront.getOtherSideId());
- assertEquals(docIdCardFront.getId(), docIdCardBack.getOtherSideId());
+ void testResubmitDocuments_missingResubmitFlag() throws Exception {
+ final IdentityVerificationEntity identityVerification = identityVerificationRepository.findById("v1").get();
+ assertNotNull(identityVerification);
+
+ final List metadata = createIdCardMetadata();
+ metadata.get(0).setOriginalDocumentId("original1");
+ metadata.get(1).setOriginalDocumentId("original2");
+ final List data = createIdCardData();
+ final OwnerId ownerId = createOwnerId();
+
+ final DocumentSubmitRequest request = new DocumentSubmitRequest();
+ request.setProcessId("p1");
+ request.setResubmit(false);
+ request.setData("files".getBytes());
+ request.setDocuments(metadata);
+ when(dataExtractionService.extractDocuments(request.getData())).thenReturn(data);
+
+ final DocumentSubmitException exception = assertThrows(DocumentSubmitException.class,
+ () -> tested.submitDocuments(identityVerification, request, ownerId));
+ assertEquals("Detected a submit request with specified originalDocumentId=original1, %s".formatted(ownerId), exception.getMessage());
+ }
+
+ private List createIdCardMetadata() {
+ final DocumentSubmitRequest.DocumentMetadata page1 = new DocumentSubmitRequest.DocumentMetadata();
+ page1.setFilename("id_card_front.png");
+ page1.setType(DocumentType.ID_CARD);
+ page1.setSide(CardSide.FRONT);
+
+ final DocumentSubmitRequest.DocumentMetadata page2 = new DocumentSubmitRequest.DocumentMetadata();
+ page2.setFilename("id_card_back.png");
+ page2.setType(DocumentType.ID_CARD);
+ page2.setSide(CardSide.BACK);
+
+ return List.of(page1, page2);
+ }
+
+ private List createIdCardData() {
+ final Document documentPage1 = new Document();
+ documentPage1.setData("img1".getBytes());
+ documentPage1.setFilename("id_card_front.png");
+
+ final Document documentPage2 = new Document();
+ documentPage2.setData("img2".getBytes());
+ documentPage2.setFilename("id_card_back.png");
+
+ return List.of(documentPage1, documentPage2);
+ }
+
+ private OwnerId createOwnerId() {
+ final OwnerId ownerId = new OwnerId();
+ ownerId.setActivationId("a1");
+ ownerId.setUserId("u1");
+ return ownerId;
}
}
diff --git a/enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingServiceTest.sql b/enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingServiceTest.sql
new file mode 100644
index 000000000..a8d5f54f3
--- /dev/null
+++ b/enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingServiceTest.sql
@@ -0,0 +1,2 @@
+INSERT INTO es_identity_verification(id, activation_id, user_id, process_id, status, phase, timestamp_created) VALUES
+ ('v1', 'a1', 'u1', 'p1', 'IN_PROGRESS', 'DOCUMENT_UPLOAD', now());
diff --git a/enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingServiceTest.testPairTwoSidedDocuments.sql b/enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingServiceTest.testPairTwoSidedDocuments.sql
new file mode 100644
index 000000000..7f5ef45c6
--- /dev/null
+++ b/enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingServiceTest.testPairTwoSidedDocuments.sql
@@ -0,0 +1,6 @@
+INSERT INTO es_identity_verification(id, activation_id, user_id, process_id, status, phase, timestamp_created) VALUES
+ ('v1', 'a1', 'u1', 'p1', 'VERIFICATION_PENDING', 'DOCUMENT_VERIFICATION', now());
+
+INSERT INTO es_document_verification(id, activation_id, identity_verification_id, type, side, status, used_for_verification, filename, timestamp_created) VALUES
+ ('1', 'a1', 'v1', 'ID_CARD', 'FRONT', 'VERIFICATION_PENDING', true, 'id_front.png', now()),
+ ('2', 'a1', 'v1', 'ID_CARD', 'BACK', 'VERIFICATION_PENDING', true, 'id_back.png', now());
diff --git a/enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingServiceTest.testResubmitDocuments.sql b/enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingServiceTest.testResubmitDocuments.sql
new file mode 100644
index 000000000..ecaf09aa7
--- /dev/null
+++ b/enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingServiceTest.testResubmitDocuments.sql
@@ -0,0 +1,6 @@
+INSERT INTO es_identity_verification(id, activation_id, user_id, process_id, status, phase, timestamp_created) VALUES
+ ('v1', 'a1', 'u1', 'p1', 'VERIFICATION_PENDING', 'DOCUMENT_VERIFICATION', now());
+
+INSERT INTO es_document_verification(id, activation_id, identity_verification_id, type, side, status, used_for_verification, filename, timestamp_created) VALUES
+ ('original1', 'a1', 'v1', 'ID_CARD', 'FRONT', 'VERIFICATION_PENDING', true, 'original_id_front.png', now()),
+ ('original2', 'a1', 'v1', 'ID_CARD', 'BACK', 'VERIFICATION_PENDING', true, 'original_id_back.png', now());
\ No newline at end of file
From 893202236f406a0caf0b6c6785fd580afbb382ca Mon Sep 17 00:00:00 2001
From: Lubos Racansky
Date: Mon, 18 Dec 2023 10:47:22 +0100
Subject: [PATCH 41/52] Fix #962: Add Migration Guide for 1.6.0
---
docs/Migration-Instructions.md | 1 +
docs/PowerAuth-Enrollment-Server-1.6.0.md | 5 +++++
docs/onboarding/Migration-Instructions.md | 1 +
.../PowerAuth-Enrollment-Onboarding-Server-1.6.0.md | 5 +++++
4 files changed, 12 insertions(+)
create mode 100644 docs/PowerAuth-Enrollment-Server-1.6.0.md
create mode 100644 docs/onboarding/PowerAuth-Enrollment-Onboarding-Server-1.6.0.md
diff --git a/docs/Migration-Instructions.md b/docs/Migration-Instructions.md
index 074891779..0bf91d4af 100644
--- a/docs/Migration-Instructions.md
+++ b/docs/Migration-Instructions.md
@@ -2,5 +2,6 @@
This page contains PowerAuth Enrollment Server migration instructions.
+- [PowerAuth Enrollment Server 1.6.0](./PowerAuth-Enrollment-Server-1.6.0.md)
- [PowerAuth Enrollment Server 1.5.0](./PowerAuth-Enrollment-Server-1.5.0.md)
- [PowerAuth Enrollment Server 1.4.0](./PowerAuth-Enrollment-Server-1.4.0.md)
diff --git a/docs/PowerAuth-Enrollment-Server-1.6.0.md b/docs/PowerAuth-Enrollment-Server-1.6.0.md
new file mode 100644
index 000000000..66ea56171
--- /dev/null
+++ b/docs/PowerAuth-Enrollment-Server-1.6.0.md
@@ -0,0 +1,5 @@
+# Migration from 1.5.x to 1.6.x
+
+This guide contains instructions for migration from PowerAuth Enrollment Server version `1.5.x` to version `1.6.0`.
+
+No migration steps nor database changes are required.
diff --git a/docs/onboarding/Migration-Instructions.md b/docs/onboarding/Migration-Instructions.md
index 21b191aa9..9c502ee33 100644
--- a/docs/onboarding/Migration-Instructions.md
+++ b/docs/onboarding/Migration-Instructions.md
@@ -2,4 +2,5 @@
This page contains PowerAuth Enrollment Onboarding Server migration instructions.
+- [PowerAuth Enrollment Onboarding Server 1.6.0](./PowerAuth-Enrollment-Onboarding-Server-1.6.0.md)
- [PowerAuth Enrollment Onboarding Server 1.5.0](./PowerAuth-Enrollment-Onboarding-Server-1.5.0.md)
diff --git a/docs/onboarding/PowerAuth-Enrollment-Onboarding-Server-1.6.0.md b/docs/onboarding/PowerAuth-Enrollment-Onboarding-Server-1.6.0.md
new file mode 100644
index 000000000..1613bb122
--- /dev/null
+++ b/docs/onboarding/PowerAuth-Enrollment-Onboarding-Server-1.6.0.md
@@ -0,0 +1,5 @@
+# Migration from 1.5.x to 1.6.x
+
+This guide contains instructions for migration from PowerAuth Enrollment Onboarding Server version `1.5.x` to version `1.6.0`.
+
+No migration steps nor database changes are required.
From 1ddfe6d974642c7a4e49c43793872834249c8d3a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Pe=C5=A1ek?=
Date: Mon, 18 Dec 2023 11:05:46 +0100
Subject: [PATCH 42/52] Fix #960: Remove code-level consistency check in
Innovatrics Document Provider (#961)
---
docs/onboarding/Configuration-Properties.md | 1 -
.../innovatrics/InnovatricsConfigProps.java | 5 -----
...novatricsDocumentVerificationProvider.java | 22 ++++---------------
.../resources/application-test.properties | 1 -
.../src/main/resources/application.properties | 1 -
5 files changed, 4 insertions(+), 26 deletions(-)
diff --git a/docs/onboarding/Configuration-Properties.md b/docs/onboarding/Configuration-Properties.md
index 1918751fb..0ae467ce7 100644
--- a/docs/onboarding/Configuration-Properties.md
+++ b/docs/onboarding/Configuration-Properties.md
@@ -144,7 +144,6 @@ The Onboarding Server uses the following public configuration properties:
| `enrollment-server-onboarding.provider.innovatrics.serviceUserAgent` | `Wultra/OnboardingServer` | User agent to use when making HTTP calls to Innovatrics REST service. |
| `enrollment-server-onboarding.provider.innovatrics.presenceCheckConfiguration.score` | 0.875 | Presence check minimal score threshold. |
| `enrollment-server-onboarding.provider.innovatrics.documentVerificationConfiguration.documentCountries` | `CZE` | List of expected countries of issue of identification documents as three-letter country codes, i.e. ISO 3166-1 alpha-3. If empty, all countries of issue known to Innovatrics are considered during classification, which may have negative impact on performance. |
-| `enrollment-server-onboarding.provider.innovatrics.document-verification-configuration.crucialFields` | `documentNumber`, `dateOfIssue`, `dateOfExpiry`, `surname`, `dateOfBirth`, `personalNumber`, `givenNames` | Set of fields in camelCase that are cross-validated between the machine-readable zone and visual zone. Only those specified fields that are actually extracted from a document are considered. If empty, no inconsistencies will be checked. |
| `enrollment-server-onboarding.provider.innovatrics.restClientConfig.acceptInvalidSslCertificate` | `false` | Whether invalid SSL certificate is accepted when calling Zen ID REST service. |
| `enrollment-server-onboarding.provider.innovatrics.restClientConfig.maxInMemorySize` | `10485760` | Maximum in memory size of HTTP requests when calling Innovatrics REST service. |
| `enrollment-server-onboarding.provider.innovatrics.restClientConfig.proxyEnabled` | `false` | Whether proxy server is enabled when calling Innovatrics REST service. |
diff --git a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsConfigProps.java b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsConfigProps.java
index ce0acde45..da7adb429 100644
--- a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsConfigProps.java
+++ b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsConfigProps.java
@@ -81,11 +81,6 @@ public static class DocumentVerificationConfiguration {
* Identifies expected document countries of issue in ISO 3166-1 alpha-3 format.
*/
private List documentCountries;
-
- /**
- * Set of fields in camelCase that are cross-validated between the machine-readable zone and visual zone, if extracted.
- */
- private Set crucialFields;
}
}
diff --git a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsDocumentVerificationProvider.java b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsDocumentVerificationProvider.java
index e3d134135..5f660561e 100644
--- a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsDocumentVerificationProvider.java
+++ b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsDocumentVerificationProvider.java
@@ -372,37 +372,23 @@ private List parseVisualZoneInspection(final VisualZoneInspection visual
final MrzConsistency mrzConsistency = textConsistentWith.getMrz();
if (mrzConsistency != null) {
- final List inconsistentAttributes = getCrucial(mrzConsistency.getInconsistentTexts());
+ final List inconsistentAttributes = mrzConsistency.getInconsistentTexts();
if (!inconsistentAttributes.isEmpty()) {
- rejectionReasons.add("Inconsistent crucial attributes with MRZ: %s".formatted(inconsistentAttributes));
+ rejectionReasons.add("Inconsistent attributes with MRZ: %s".formatted(inconsistentAttributes));
}
}
final BarcodesConsistency barcodesConsistency = textConsistentWith.getBarcodes();
if (barcodesConsistency != null) {
- final List inconsistentAttributes = getCrucial(barcodesConsistency.getInconsistentTexts());
+ final List inconsistentAttributes = barcodesConsistency.getInconsistentTexts();
if (!inconsistentAttributes.isEmpty()) {
- rejectionReasons.add("Inconsistent crucial attributes with barcode: %s".formatted(inconsistentAttributes));
+ rejectionReasons.add("Inconsistent attributes with barcode: %s".formatted(inconsistentAttributes));
}
}
return rejectionReasons;
}
- /**
- * Intersects list of attributes with CRUCIAL_ATTRIBUTES.
- * @param attributes Attributes to do the intersection on.
- * @return Attributes intersection.
- */
- private List getCrucial(final List attributes) {
- if (attributes == null) {
- return Collections.emptyList();
- }
-
- final Set crucialFields = configuration.getDocumentVerificationConfiguration().getCrucialFields();
- return attributes.stream().filter(crucialFields::contains).toList();
- }
-
private String serializeToString(T src) throws DocumentVerificationException {
try {
return objectMapper.writeValueAsString(src);
diff --git a/enrollment-server-onboarding-provider-innovatrics/src/test/resources/application-test.properties b/enrollment-server-onboarding-provider-innovatrics/src/test/resources/application-test.properties
index 8e68187cc..4c7f63087 100644
--- a/enrollment-server-onboarding-provider-innovatrics/src/test/resources/application-test.properties
+++ b/enrollment-server-onboarding-provider-innovatrics/src/test/resources/application-test.properties
@@ -3,6 +3,5 @@ enrollment-server-onboarding.presence-check.provider=innovatrics
enrollment-server-onboarding.onboarding-process.enabled=false
enrollment-server-onboarding.provider.innovatrics.documentVerificationConfiguration.documentCountries=CZE,SVK
-enrollment-server-onboarding.provider.innovatrics.document-verification-configuration.crucialFields=documentNumber, dateOfIssue, dateOfExpiry, surname, dateOfBirth, personalNumber, givenNames
enrollment-server-onboarding.provider.innovatrics.restClientConfig.maxInMemorySize=1048576
\ No newline at end of file
diff --git a/enrollment-server-onboarding/src/main/resources/application.properties b/enrollment-server-onboarding/src/main/resources/application.properties
index 9b608a5d4..29f2fcf68 100644
--- a/enrollment-server-onboarding/src/main/resources/application.properties
+++ b/enrollment-server-onboarding/src/main/resources/application.properties
@@ -177,7 +177,6 @@ enrollment-server-onboarding.provider.innovatrics.presenceCheckConfiguration.sco
# Innovatrics document-verification configuration
enrollment-server-onboarding.provider.innovatrics.documentVerificationConfiguration.documentCountries=CZE
-enrollment-server-onboarding.provider.innovatrics.document-verification-configuration.crucialFields=documentNumber, dateOfIssue, dateOfExpiry, surname, dateOfBirth, personalNumber, givenNames
# Innovatrics REST client configuration
enrollment-server-onboarding.provider.innovatrics.restClientConfig.acceptInvalidSslCertificate=false
From 3a54da3c1c64bdb00d001e26f8aeb5d73f6acf49 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Pe=C5=A1ek?=
Date: Mon, 18 Dec 2023 11:15:41 +0100
Subject: [PATCH 43/52] Fix #956: Respect provider name in queries (#957)
---
enrollment-server-onboarding-common/pom.xml | 6 ++
.../database/DocumentResultRepository.java | 6 +-
...ServerOnboardingCommonTestApplication.java | 24 +++++++
.../DocumentResultRepositoryTest.java | 65 +++++++++++++++++
.../resources/application-test.properties | 5 ++
...lInProgressDocumentSubmitVerifications.sql | 12 ++++
...testStreamAllInProgressDocumentSubmits.sql | 12 ++++
.../DocumentProcessingBatchService.java | 18 ++---
.../VerificationProcessingBatchService.java | 34 ++-------
.../service/StateMachineService.java | 61 ++++++----------
.../service/PresenceCheckServiceTest.java | 11 ---
.../service/StateMachineServiceTest.java | 70 +++++++++++++++++++
.../resources/application-test.properties | 2 +
...iceTest.testChangeMachineStatesInBatch.sql | 6 ++
...ChangeMachineStatesInBatch_noDocuments.sql | 4 ++
...tChangeMachineStatesInBatch_submitting.sql | 6 ++
16 files changed, 249 insertions(+), 93 deletions(-)
create mode 100644 enrollment-server-onboarding-common/src/test/java/com/wultra/app/onboardingserver/common/EnrollmentServerOnboardingCommonTestApplication.java
create mode 100644 enrollment-server-onboarding-common/src/test/java/com/wultra/app/onboardingserver/common/database/DocumentResultRepositoryTest.java
create mode 100644 enrollment-server-onboarding-common/src/test/resources/application-test.properties
create mode 100644 enrollment-server-onboarding-common/src/test/resources/com/wultra/app/onboardingserver/common/database/DocumentResultRepositoryTest.testStreamAllInProgressDocumentSubmitVerifications.sql
create mode 100644 enrollment-server-onboarding-common/src/test/resources/com/wultra/app/onboardingserver/common/database/DocumentResultRepositoryTest.testStreamAllInProgressDocumentSubmits.sql
create mode 100644 enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/statemachine/service/StateMachineServiceTest.java
create mode 100644 enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/statemachine/service/StateMachineServiceTest.testChangeMachineStatesInBatch.sql
create mode 100644 enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/statemachine/service/StateMachineServiceTest.testChangeMachineStatesInBatch_noDocuments.sql
create mode 100644 enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/statemachine/service/StateMachineServiceTest.testChangeMachineStatesInBatch_submitting.sql
diff --git a/enrollment-server-onboarding-common/pom.xml b/enrollment-server-onboarding-common/pom.xml
index 09f6359c5..d6d88b13e 100644
--- a/enrollment-server-onboarding-common/pom.xml
+++ b/enrollment-server-onboarding-common/pom.xml
@@ -61,6 +61,12 @@
spring-boot-starter-test
test
+
+
+ com.h2database
+ h2
+ test
+
diff --git a/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/DocumentResultRepository.java b/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/DocumentResultRepository.java
index cb0eab250..fdcc339ba 100644
--- a/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/DocumentResultRepository.java
+++ b/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/DocumentResultRepository.java
@@ -40,18 +40,20 @@ public interface DocumentResultRepository extends CrudRepository streamAllInProgressDocumentSubmits();
+ Stream streamAllInProgressDocumentSubmits(String providerName);
/**
* @return All not finished document submit verifications (upload is in progress and verification id exists)
*/
@Query("SELECT doc FROM DocumentResultEntity doc WHERE" +
" doc.documentVerification.status = com.wultra.app.enrollmentserver.model.enumeration.DocumentStatus.UPLOAD_IN_PROGRESS" +
+ " AND doc.documentVerification.providerName = :providerName " +
" AND doc.documentVerification.verificationId IS NOT NULL" +
" ORDER BY doc.timestampCreated ASC")
- Stream streamAllInProgressDocumentSubmitVerifications();
+ Stream streamAllInProgressDocumentSubmitVerifications(String providerName);
/**
* @return All document results for the specified document verification and processing phase
diff --git a/enrollment-server-onboarding-common/src/test/java/com/wultra/app/onboardingserver/common/EnrollmentServerOnboardingCommonTestApplication.java b/enrollment-server-onboarding-common/src/test/java/com/wultra/app/onboardingserver/common/EnrollmentServerOnboardingCommonTestApplication.java
new file mode 100644
index 000000000..437b51434
--- /dev/null
+++ b/enrollment-server-onboarding-common/src/test/java/com/wultra/app/onboardingserver/common/EnrollmentServerOnboardingCommonTestApplication.java
@@ -0,0 +1,24 @@
+/*
+ * PowerAuth Enrollment Server
+ * Copyright (C) 2023 Wultra s.r.o.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.wultra.app.onboardingserver.common;
+
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class EnrollmentServerOnboardingCommonTestApplication {
+}
diff --git a/enrollment-server-onboarding-common/src/test/java/com/wultra/app/onboardingserver/common/database/DocumentResultRepositoryTest.java b/enrollment-server-onboarding-common/src/test/java/com/wultra/app/onboardingserver/common/database/DocumentResultRepositoryTest.java
new file mode 100644
index 000000000..c95706d24
--- /dev/null
+++ b/enrollment-server-onboarding-common/src/test/java/com/wultra/app/onboardingserver/common/database/DocumentResultRepositoryTest.java
@@ -0,0 +1,65 @@
+/*
+ * PowerAuth Enrollment Server
+ * Copyright (C) 2023 Wultra s.r.o.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.wultra.app.onboardingserver.common.database;
+
+
+import com.wultra.app.onboardingserver.common.database.entity.DocumentResultEntity;
+import com.wultra.app.onboardingserver.common.database.entity.DocumentVerificationEntity;
+import jakarta.transaction.Transactional;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.jdbc.Sql;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Test for {@link DocumentResultRepository}.
+ *
+ * @author Jan Pesek, jan.pesek@wultra.com
+ */
+@DataJpaTest
+@ActiveProfiles("test")
+@Transactional
+class DocumentResultRepositoryTest {
+
+ @Autowired
+ private DocumentResultRepository tested;
+
+ @Test
+ @Sql
+ void testStreamAllInProgressDocumentSubmits() {
+ assertThat(tested.streamAllInProgressDocumentSubmits("mock"))
+ .extracting(DocumentResultEntity::getDocumentVerification)
+ .extracting(DocumentVerificationEntity::getProviderName)
+ .containsOnly("mock")
+ .hasSize(1);
+ }
+
+ @Test
+ @Sql
+ void testStreamAllInProgressDocumentSubmitVerifications() {
+ assertThat(tested.streamAllInProgressDocumentSubmitVerifications("mock"))
+ .extracting(DocumentResultEntity::getDocumentVerification)
+ .extracting(DocumentVerificationEntity::getProviderName)
+ .containsOnly("mock")
+ .hasSize(1);
+ }
+
+}
diff --git a/enrollment-server-onboarding-common/src/test/resources/application-test.properties b/enrollment-server-onboarding-common/src/test/resources/application-test.properties
new file mode 100644
index 000000000..308c2b3ac
--- /dev/null
+++ b/enrollment-server-onboarding-common/src/test/resources/application-test.properties
@@ -0,0 +1,5 @@
+spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_ON_EXIT=FALSE
+spring.datasource.username=sa
+spring.datasource.password=password
+spring.datasource.driver-class-name=org.h2.Driver
+spring.jpa.hibernate.ddl-auto=create
diff --git a/enrollment-server-onboarding-common/src/test/resources/com/wultra/app/onboardingserver/common/database/DocumentResultRepositoryTest.testStreamAllInProgressDocumentSubmitVerifications.sql b/enrollment-server-onboarding-common/src/test/resources/com/wultra/app/onboardingserver/common/database/DocumentResultRepositoryTest.testStreamAllInProgressDocumentSubmitVerifications.sql
new file mode 100644
index 000000000..a6affd76d
--- /dev/null
+++ b/enrollment-server-onboarding-common/src/test/resources/com/wultra/app/onboardingserver/common/database/DocumentResultRepositoryTest.testStreamAllInProgressDocumentSubmitVerifications.sql
@@ -0,0 +1,12 @@
+-- Documents that have been already submitted and data were extracted (multiple providers).
+INSERT INTO es_identity_verification(id, activation_id, user_id, process_id, status, phase, timestamp_created, timestamp_last_updated) VALUES
+('v3', 'a3', 'u3', 'p3', 'IN_PROGRESS', 'DOCUMENT_UPLOAD', now(), now()),
+('v4', 'a4', 'u4', 'p4', 'IN_PROGRESS', 'DOCUMENT_UPLOAD', now(), now());
+
+INSERT INTO es_document_verification(id, provider_name, activation_id, identity_verification_id, verification_id, type, status, filename, used_for_verification, timestamp_created, timestamp_last_updated) VALUES
+ ('d3', 'foreign', 'a3', 'v3', 'verification1', 'ID_CARD', 'UPLOAD_IN_PROGRESS', 'f3', true, now(), now()),
+ ('d4', 'mock', 'a4', 'v4', 'verification2', 'ID_CARD', 'UPLOAD_IN_PROGRESS', 'f4', true, now(), now());
+
+INSERT INTO es_document_result(id, document_verification_id, phase, extracted_data, timestamp_created) VALUES
+ (3, 'd3', 'UPLOAD', '{extracted_data}', now()),
+ (4, 'd4', 'UPLOAD', '{extracted_data}', now());
diff --git a/enrollment-server-onboarding-common/src/test/resources/com/wultra/app/onboardingserver/common/database/DocumentResultRepositoryTest.testStreamAllInProgressDocumentSubmits.sql b/enrollment-server-onboarding-common/src/test/resources/com/wultra/app/onboardingserver/common/database/DocumentResultRepositoryTest.testStreamAllInProgressDocumentSubmits.sql
new file mode 100644
index 000000000..e32e438eb
--- /dev/null
+++ b/enrollment-server-onboarding-common/src/test/resources/com/wultra/app/onboardingserver/common/database/DocumentResultRepositoryTest.testStreamAllInProgressDocumentSubmits.sql
@@ -0,0 +1,12 @@
+-- Documents that have not been submitted yet (multiple providers).
+INSERT INTO es_identity_verification(id, activation_id, user_id, process_id, status, phase, timestamp_created, timestamp_last_updated) VALUES
+ ('v1', 'a1', 'u1', 'p1', 'IN_PROGRESS', 'DOCUMENT_UPLOAD', now(), now()),
+ ('v2', 'a2', 'u2', 'p2', 'IN_PROGRESS', 'DOCUMENT_UPLOAD', now(), now());
+
+INSERT INTO es_document_verification(id, provider_name, activation_id, identity_verification_id, type, status, filename, used_for_verification, timestamp_created, timestamp_last_updated) VALUES
+ ('d1', 'foreign', 'a1', 'v1', 'ID_CARD', 'UPLOAD_IN_PROGRESS', 'f1', true, now(), now()),
+ ('d2', 'mock', 'a2', 'v2', 'ID_CARD', 'UPLOAD_IN_PROGRESS', 'f2', true, now(), now());
+
+INSERT INTO es_document_result(id, document_verification_id, phase, timestamp_created) VALUES
+ (1, 'd1', 'UPLOAD', now()),
+ (2, 'd2', 'UPLOAD', now());
diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingBatchService.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingBatchService.java
index cb3cda261..2ffb2e34a 100644
--- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingBatchService.java
+++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingBatchService.java
@@ -22,6 +22,8 @@
import com.wultra.app.onboardingserver.common.database.entity.DocumentVerificationEntity;
import com.wultra.app.enrollmentserver.model.enumeration.DocumentStatus;
import com.wultra.app.enrollmentserver.model.integration.OwnerId;
+import com.wultra.app.onboardingserver.configuration.IdentityVerificationConfig;
+import lombok.AllArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -37,6 +39,7 @@
* @author Lukas Lukovsky, lukas.lukovsky@wultra.com
*/
@Service
+@AllArgsConstructor
public class DocumentProcessingBatchService {
private static final Logger logger = LoggerFactory.getLogger(DocumentProcessingBatchService.class);
@@ -45,18 +48,7 @@ public class DocumentProcessingBatchService {
private final DocumentProcessingService documentProcessingService;
- /**
- * Service constructor.
- * @param documentResultRepository Document verification result repository.
- * @param documentProcessingService Document processing service.
- */
- @Autowired
- public DocumentProcessingBatchService(
- DocumentResultRepository documentResultRepository,
- DocumentProcessingService documentProcessingService) {
- this.documentResultRepository = documentResultRepository;
- this.documentProcessingService = documentProcessingService;
- }
+ private final IdentityVerificationConfig identityVerificationConfig;
/**
* Checks in progress document submits on current provider status and data result
@@ -64,7 +56,7 @@ public DocumentProcessingBatchService(
@Transactional
public void checkInProgressDocumentSubmits() {
AtomicInteger countFinished = new AtomicInteger(0);
- try (Stream stream = documentResultRepository.streamAllInProgressDocumentSubmits()) {
+ try (Stream stream = documentResultRepository.streamAllInProgressDocumentSubmits(identityVerificationConfig.getDocumentVerificationProvider())) {
stream.forEach(docResult -> {
DocumentVerificationEntity docVerification = docResult.getDocumentVerification();
final OwnerId ownerId = new OwnerId();
diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/verification/VerificationProcessingBatchService.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/verification/VerificationProcessingBatchService.java
index fb6282b5f..34672929b 100644
--- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/verification/VerificationProcessingBatchService.java
+++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/verification/VerificationProcessingBatchService.java
@@ -31,8 +31,10 @@
import com.wultra.app.onboardingserver.common.service.AuditService;
import com.wultra.app.onboardingserver.common.service.CommonOnboardingService;
import com.wultra.app.onboardingserver.api.errorhandling.DocumentVerificationException;
+import com.wultra.app.onboardingserver.configuration.IdentityVerificationConfig;
import com.wultra.app.onboardingserver.impl.service.IdentityVerificationService;
import com.wultra.app.onboardingserver.api.provider.DocumentVerificationProvider;
+import lombok.AllArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -49,6 +51,7 @@
* @author Lukas Lukovsky, lukas.lukovsky@wultra.com
*/
@Service
+@AllArgsConstructor
public class VerificationProcessingBatchService {
private static final Logger logger = LoggerFactory.getLogger(VerificationProcessingBatchService.class);
@@ -67,34 +70,7 @@ public class VerificationProcessingBatchService {
private final CommonOnboardingService commonOnboardingService;
- /**
- * Service constructor.
- * @param documentResultRepository Document verification result repository.
- * @param documentVerificationProvider Document verification provider.
- * @param identityVerificationRepository Identity verification repository.
- * @param identityVerificationService Identity verification service.
- * @param verificationProcessingService Verification processing service.
- * @param auditService Audit service.
- * @param commonOnboardingService Onboarding process service (common).
- */
- @Autowired
- public VerificationProcessingBatchService(
- final DocumentResultRepository documentResultRepository,
- final DocumentVerificationProvider documentVerificationProvider,
- final IdentityVerificationRepository identityVerificationRepository,
- final IdentityVerificationService identityVerificationService,
- final VerificationProcessingService verificationProcessingService,
- final AuditService auditService,
- final CommonOnboardingService commonOnboardingService) {
-
- this.documentResultRepository = documentResultRepository;
- this.identityVerificationRepository = identityVerificationRepository;
- this.documentVerificationProvider = documentVerificationProvider;
- this.identityVerificationService = identityVerificationService;
- this.verificationProcessingService = verificationProcessingService;
- this.auditService = auditService;
- this.commonOnboardingService = commonOnboardingService;
- }
+ private final IdentityVerificationConfig identityVerificationConfig;
/**
* Checks document submit verifications
@@ -102,7 +78,7 @@ public VerificationProcessingBatchService(
@Transactional
public void checkDocumentSubmitVerifications() {
AtomicInteger countFinished = new AtomicInteger(0);
- try (Stream stream = documentResultRepository.streamAllInProgressDocumentSubmitVerifications()) {
+ try (Stream stream = documentResultRepository.streamAllInProgressDocumentSubmitVerifications(identityVerificationConfig.getDocumentVerificationProvider())) {
stream.forEach(docResult -> {
DocumentVerificationEntity docVerification = docResult.getDocumentVerification();
diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/statemachine/service/StateMachineService.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/statemachine/service/StateMachineService.java
index b871dc4fe..5b1357537 100644
--- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/statemachine/service/StateMachineService.java
+++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/statemachine/service/StateMachineService.java
@@ -19,6 +19,7 @@
import com.wultra.app.enrollmentserver.model.integration.OwnerId;
import com.wultra.app.onboardingserver.common.database.entity.IdentityVerificationEntity;
import com.wultra.app.onboardingserver.common.errorhandling.IdentityVerificationException;
+import com.wultra.app.onboardingserver.configuration.IdentityVerificationConfig;
import com.wultra.app.onboardingserver.impl.service.IdentityVerificationService;
import com.wultra.app.onboardingserver.statemachine.EnrollmentStateProvider;
import com.wultra.app.onboardingserver.statemachine.consts.EventHeaderName;
@@ -27,6 +28,7 @@
import com.wultra.app.onboardingserver.statemachine.enums.OnboardingState;
import com.wultra.app.onboardingserver.statemachine.interceptor.CustomStateMachineInterceptor;
import jakarta.annotation.Nullable;
+import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.messaging.Message;
@@ -53,6 +55,7 @@
*/
@Service
@Slf4j
+@AllArgsConstructor
@ConditionalOnProperty(value = "enrollment-server-onboarding.identity-verification.enabled", havingValue = "true")
public class StateMachineService {
@@ -66,27 +69,7 @@ public class StateMachineService {
private final TransactionTemplate transactionTemplate;
- /**
- * Constructor.
- *
- * @param enrollmentStateProvider Enrollment state provider.
- * @param stateMachineFactory State machine factory.
- * @param stateMachineInterceptor State machine interceptor.
- * @param identityVerificationService Identity verification service.
- */
- public StateMachineService(
- final EnrollmentStateProvider enrollmentStateProvider,
- final StateMachineFactory stateMachineFactory,
- final CustomStateMachineInterceptor stateMachineInterceptor,
- final IdentityVerificationService identityVerificationService,
- final TransactionTemplate transactionTemplate
- ) {
- this.enrollmentStateProvider = enrollmentStateProvider;
- this.stateMachineFactory = stateMachineFactory;
- this.stateMachineInterceptor = stateMachineInterceptor;
- this.identityVerificationService = identityVerificationService;
- this.transactionTemplate = transactionTemplate;
- }
+ private final IdentityVerificationConfig identityVerificationConfig;
@Transactional
public StateMachine processStateMachineEvent(OwnerId ownerId, String processId, OnboardingEvent event)
@@ -144,23 +127,25 @@ public Message createMessage(OwnerId ownerId, String processId,
public void changeMachineStatesInBatch() {
final AtomicInteger countFinished = new AtomicInteger(0);
try (Stream stream = identityVerificationService.streamAllIdentityVerificationsToChangeState().parallel()) {
- stream.forEach(identityVerification -> {
- final String processId = identityVerification.getProcessId();
- final OwnerId ownerId = new OwnerId();
- ownerId.setActivationId(identityVerification.getActivationId());
- ownerId.setUserId(identityVerification.getUserId());
- logger.debug("Changing state of machine for process ID: {}", processId);
-
- transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
- transactionTemplate.executeWithoutResult(status -> {
- try {
- processStateMachineEvent(ownerId, processId, OnboardingEvent.EVENT_NEXT_STATE);
- countFinished.incrementAndGet();
- } catch (IdentityVerificationException e) {
- logger.warn("Unable to change state for process ID: {}", processId, e);
- }
- });
- });
+ stream.filter(identityVerification -> identityVerification.getDocumentVerifications().stream()
+ .anyMatch(doc -> identityVerificationConfig.getDocumentVerificationProvider().equals(doc.getProviderName())))
+ .forEach(identityVerification -> {
+ final String processId = identityVerification.getProcessId();
+ final OwnerId ownerId = new OwnerId();
+ ownerId.setActivationId(identityVerification.getActivationId());
+ ownerId.setUserId(identityVerification.getUserId());
+ logger.debug("Changing state of machine for process ID: {}", processId);
+
+ transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
+ transactionTemplate.executeWithoutResult(status -> {
+ try {
+ processStateMachineEvent(ownerId, processId, OnboardingEvent.EVENT_NEXT_STATE);
+ countFinished.incrementAndGet();
+ } catch (IdentityVerificationException e) {
+ logger.warn("Unable to change state for process ID: {}", processId, e);
+ }
+ });
+ });
}
if (countFinished.get() > 0) {
logger.debug("Changed state of {} identity verifications", countFinished.get());
diff --git a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckServiceTest.java b/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckServiceTest.java
index c256c6d5d..93ea22fbe 100644
--- a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckServiceTest.java
+++ b/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckServiceTest.java
@@ -18,34 +18,23 @@
import com.wultra.app.enrollmentserver.model.enumeration.CardSide;
import com.wultra.app.enrollmentserver.model.enumeration.DocumentType;
-import com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationPhase;
-import com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationStatus;
import com.wultra.app.enrollmentserver.model.integration.Image;
import com.wultra.app.enrollmentserver.model.integration.OwnerId;
import com.wultra.app.enrollmentserver.model.integration.SessionInfo;
import com.wultra.app.onboardingserver.EnrollmentServerTestApplication;
import com.wultra.app.onboardingserver.api.provider.PresenceCheckProvider;
import com.wultra.app.onboardingserver.common.database.DocumentVerificationRepository;
-import com.wultra.app.onboardingserver.common.database.IdentityVerificationRepository;
-import com.wultra.app.onboardingserver.common.database.ScaResultRepository;
import com.wultra.app.onboardingserver.common.database.entity.DocumentVerificationEntity;
import com.wultra.app.onboardingserver.common.database.entity.IdentityVerificationEntity;
-import com.wultra.app.onboardingserver.impl.service.internal.JsonSerializationService;
import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
-import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles;
import java.util.List;
import java.util.Optional;
-import static com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationPhase.DOCUMENT_VERIFICATION_FINAL;
import static com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationPhase.PRESENCE_CHECK;
import static com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationStatus.*;
import static org.junit.jupiter.api.Assertions.assertTrue;
diff --git a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/statemachine/service/StateMachineServiceTest.java b/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/statemachine/service/StateMachineServiceTest.java
new file mode 100644
index 000000000..9972b40bd
--- /dev/null
+++ b/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/statemachine/service/StateMachineServiceTest.java
@@ -0,0 +1,70 @@
+/*
+ * PowerAuth Enrollment Server
+ * Copyright (C) 2023 Wultra s.r.o.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.wultra.app.onboardingserver.statemachine.service;
+
+import com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationStatus;
+import com.wultra.app.onboardingserver.EnrollmentServerTestApplication;
+import com.wultra.app.onboardingserver.common.database.IdentityVerificationRepository;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.jdbc.Sql;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Test for {@link StateMachineService}
+ *
+ * @author Jan Pesek, jan.pesek@wultra.com
+ */
+@SpringBootTest(classes = EnrollmentServerTestApplication.class)
+@ActiveProfiles("test")
+class StateMachineServiceTest {
+
+ @Autowired
+ private StateMachineService tested;
+
+ @Autowired
+ private IdentityVerificationRepository repository;
+
+ @Test
+ @Sql
+ void testChangeMachineStatesInBatch() {
+ tested.changeMachineStatesInBatch();
+
+ assertEquals(IdentityVerificationStatus.VERIFICATION_PENDING, repository.findById("v1").get().getStatus());
+ }
+
+ @Test
+ @Sql
+ void testChangeMachineStatesInBatch_submitting() {
+ tested.changeMachineStatesInBatch();
+
+ assertEquals(IdentityVerificationStatus.IN_PROGRESS, repository.findById("v2").get().getStatus());
+ }
+
+ @Test
+ @Sql
+ void testChangeMachineStatesInBatch_noDocuments() {
+ tested.changeMachineStatesInBatch();
+
+ assertEquals(IdentityVerificationStatus.IN_PROGRESS, repository.findById("v3").get().getStatus());
+ }
+
+}
diff --git a/enrollment-server-onboarding/src/test/resources/application-test.properties b/enrollment-server-onboarding/src/test/resources/application-test.properties
index 50a2b529f..5dff6a04e 100644
--- a/enrollment-server-onboarding/src/test/resources/application-test.properties
+++ b/enrollment-server-onboarding/src/test/resources/application-test.properties
@@ -24,3 +24,5 @@ spring.jpa.hibernate.ddl-auto=create
spring.liquibase.enabled=false
enrollment-server-onboarding.identity-verification.enabled=true
+enrollment-server-onboarding.document-verification.provider=mock
+
diff --git a/enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/statemachine/service/StateMachineServiceTest.testChangeMachineStatesInBatch.sql b/enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/statemachine/service/StateMachineServiceTest.testChangeMachineStatesInBatch.sql
new file mode 100644
index 000000000..740d005e4
--- /dev/null
+++ b/enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/statemachine/service/StateMachineServiceTest.testChangeMachineStatesInBatch.sql
@@ -0,0 +1,6 @@
+INSERT INTO es_identity_verification(id, activation_id, user_id, process_id, status, phase, timestamp_created, timestamp_last_updated) VALUES
+ ('v1', 'a1', 'u1', 'p1', 'IN_PROGRESS', 'DOCUMENT_UPLOAD', now(), now());
+
+-- document already submitted to 'mock' provider
+INSERT INTO es_document_verification(id, activation_id, identity_verification_id, type, provider_name, status, filename, used_for_verification, timestamp_created, timestamp_last_updated) VALUES
+ ('doc1', 'a1', 'v1', 'ID_CARD', 'mock', 'VERIFICATION_PENDING', 'f2', true, now(), now());
diff --git a/enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/statemachine/service/StateMachineServiceTest.testChangeMachineStatesInBatch_noDocuments.sql b/enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/statemachine/service/StateMachineServiceTest.testChangeMachineStatesInBatch_noDocuments.sql
new file mode 100644
index 000000000..fb63b4bdf
--- /dev/null
+++ b/enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/statemachine/service/StateMachineServiceTest.testChangeMachineStatesInBatch_noDocuments.sql
@@ -0,0 +1,4 @@
+INSERT INTO es_identity_verification(id, activation_id, user_id, process_id, status, phase, timestamp_created, timestamp_last_updated) VALUES
+ ('v3', 'a3', 'u3', 'p3', 'IN_PROGRESS', 'DOCUMENT_UPLOAD', now(), now());
+
+-- no documents submitted yet
diff --git a/enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/statemachine/service/StateMachineServiceTest.testChangeMachineStatesInBatch_submitting.sql b/enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/statemachine/service/StateMachineServiceTest.testChangeMachineStatesInBatch_submitting.sql
new file mode 100644
index 000000000..5cf138318
--- /dev/null
+++ b/enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/statemachine/service/StateMachineServiceTest.testChangeMachineStatesInBatch_submitting.sql
@@ -0,0 +1,6 @@
+INSERT INTO es_identity_verification(id, activation_id, user_id, process_id, status, phase, timestamp_created, timestamp_last_updated) VALUES
+ ('v2', 'a2', 'u2', 'p2', 'IN_PROGRESS', 'DOCUMENT_UPLOAD', now(), now());
+
+-- document is being submitted to a provider
+INSERT INTO es_document_verification(id, activation_id, identity_verification_id, type, provider_name, status, filename, used_for_verification, timestamp_created, timestamp_last_updated) VALUES
+ ('doc2', 'a2', 'v2', 'ID_CARD', null, 'UPLOAD_IN_PROGRESS', 'f1', true, now(), now());
From dd7cf0a4083332f6e07e9e60fe0825bfb4d8d22d Mon Sep 17 00:00:00 2001
From: Lubos Racansky
Date: Thu, 21 Dec 2023 07:19:14 +0100
Subject: [PATCH 44/52] Fix #968: MonetaryConverter throws an exception for
currency '...'
---
.../impl/service/converter/MonetaryConverter.java | 4 ++--
.../impl/service/converter/MonetaryConverterTest.java | 9 ++++++---
2 files changed, 8 insertions(+), 5 deletions(-)
diff --git a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/converter/MonetaryConverter.java b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/converter/MonetaryConverter.java
index 35022cb83..3b9321ba2 100644
--- a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/converter/MonetaryConverter.java
+++ b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/converter/MonetaryConverter.java
@@ -21,7 +21,7 @@
import javax.money.CurrencyUnit;
import javax.money.Monetary;
-import javax.money.UnknownCurrencyException;
+import javax.money.MonetaryException;
import java.math.RoundingMode;
import java.text.NumberFormat;
import java.util.Currency;
@@ -113,7 +113,7 @@ private static int getFractionDigits(String code) {
try {
final CurrencyUnit currencyUnit = Monetary.getCurrency(code);
return currencyUnit.getDefaultFractionDigits();
- } catch (UnknownCurrencyException e) {
+ } catch (MonetaryException e) {
logger.debug("No currency mapping for code={}, most probably not FIAT", code);
logger.trace("No currency mapping for code={}", code, e);
return DEFAULT_MINIMAL_FRACTION_DIGITS;
diff --git a/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/converter/MonetaryConverterTest.java b/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/converter/MonetaryConverterTest.java
index 4760d9bec..38e245b9d 100644
--- a/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/converter/MonetaryConverterTest.java
+++ b/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/converter/MonetaryConverterTest.java
@@ -47,7 +47,8 @@ class MonetaryConverterTest {
"NZD, en, NZ$",
"NZD, cs, NZ$",
"NZD, nz, NZ$",
- "BTC, cs, BTC"
+ "BTC, cs, BTC",
+ "..., cs, ..."
})
void testFormatCurrency(final String source, final String locale, final String expected) {
final String result = MonetaryConverter.formatCurrency(source, new Locale(locale));
@@ -68,7 +69,8 @@ void testFormatCurrency(final String source, final String locale, final String e
"1, BTC, en, '1'",
"1.1, BTC, en, '1.1'",
"0.123456789, BTC, en, '0.123456789'",
- "0.567567567567567567567, BTC, en, '0.567567567567567567'"
+ "0.567567567567567567567, BTC, en, '0.567567567567567567'",
+ "1, ..., en, '1'"
})
void testFormatAmount(final String amount, final String code, final String locale, final String expected) {
final String result = MonetaryConverter.formatAmount(new BigDecimal(amount), code, new Locale(locale));
@@ -92,7 +94,8 @@ void testFormatAmount(final String amount, final String code, final String local
"1, BTC, en, '1 BTC'",
"1.1, BTC, en, '1.1 BTC'",
"0.123456789, BTC, en, '0.123456789 BTC'",
- "0.567567567567567567567, BTC, en, '0.567567567567567567 BTC'"
+ "0.567567567567567567567, BTC, en, '0.567567567567567567 BTC'",
+ "1, ..., en, '1 ...'"
})
void testFormatValue(final String amount, final String code, final String locale, final String expected) {
final String result = MonetaryConverter.formatValue(new BigDecimal(amount), code, new Locale(locale));
From 059a9269f5c9a5a8f0d64a7d677304acd9d7739f Mon Sep 17 00:00:00 2001
From: Lubos Racansky
Date: Thu, 21 Dec 2023 08:51:07 +0100
Subject: [PATCH 45/52] Fix #966: Add micrometer-registry-prometheus
---
docs/Configuration-Properties.md | 6 ++++++
docs/onboarding/Configuration-Properties.md | 6 ++++++
enrollment-server-onboarding/pom.xml | 6 ++++++
.../src/main/resources/application.properties | 8 +++++++-
enrollment-server/pom.xml | 6 ++++++
.../src/main/resources/application.properties | 8 +++++++-
6 files changed, 38 insertions(+), 2 deletions(-)
diff --git a/docs/Configuration-Properties.md b/docs/Configuration-Properties.md
index 9a0295a8d..46718a263 100644
--- a/docs/Configuration-Properties.md
+++ b/docs/Configuration-Properties.md
@@ -60,3 +60,9 @@ Sample setting of logging pattern:
```properties
logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:%5p}) [%X{X-Correlation-ID}] %clr(%5p) %clr(${PID: }){magenta} %clr(---){faint}%clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}
```
+
+
+## Monitoring and Observability
+
+The WAR file includes the `micrometer-registry-prometheus` dependency.
+Discuss its configuration with the [Spring Boot documentation](https://docs.spring.io/spring-boot/docs/3.1.x/reference/html/actuator.html#actuator.metrics).
diff --git a/docs/onboarding/Configuration-Properties.md b/docs/onboarding/Configuration-Properties.md
index 0ae467ce7..799d45a15 100644
--- a/docs/onboarding/Configuration-Properties.md
+++ b/docs/onboarding/Configuration-Properties.md
@@ -167,3 +167,9 @@ Sample setting of logging pattern:
```properties
logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:%5p}) [%X{X-Correlation-ID}] %clr(%5p) %clr(${PID: }){magenta} %clr(---){faint}%clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}
```
+
+
+## Monitoring and Observability
+
+The WAR file includes the `micrometer-registry-prometheus` dependency.
+Discuss its configuration with the [Spring Boot documentation](https://docs.spring.io/spring-boot/docs/3.1.x/reference/html/actuator.html#actuator.metrics).
diff --git a/enrollment-server-onboarding/pom.xml b/enrollment-server-onboarding/pom.xml
index 10ba0fb81..3c9f79bb3 100644
--- a/enrollment-server-onboarding/pom.xml
+++ b/enrollment-server-onboarding/pom.xml
@@ -142,6 +142,12 @@
logstash-logback-encoder
+
+
+ io.micrometer
+ micrometer-registry-prometheus
+
+
org.springframework.boot
diff --git a/enrollment-server-onboarding/src/main/resources/application.properties b/enrollment-server-onboarding/src/main/resources/application.properties
index 29f2fcf68..09f3c82f0 100644
--- a/enrollment-server-onboarding/src/main/resources/application.properties
+++ b/enrollment-server-onboarding/src/main/resources/application.properties
@@ -206,4 +206,10 @@ powerauth.service.correlation-header.enabled=false
powerauth.service.correlation-header.name=X-Correlation-ID
powerauth.service.correlation-header.value.validation-regexp=[a-zA-Z0-9\\-]{8,1024}
# For logging correlation HTTP headers enable the pattern and update correlation header name in the pattern
-#logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:%5p}) [%X{X-Correlation-ID}] %clr(%5p) %clr(${PID: }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}
\ No newline at end of file
+#logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:%5p}) [%X{X-Correlation-ID}] %clr(%5p) %clr(${PID: }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}
+
+# Monitoring
+#management.endpoint.metrics.enabled=true
+#management.endpoints.web.exposure.include=health, prometheus
+#management.endpoint.prometheus.enabled=true
+#management.prometheus.metrics.export.enabled=true
diff --git a/enrollment-server/pom.xml b/enrollment-server/pom.xml
index 1da830ea3..bd0e7e310 100644
--- a/enrollment-server/pom.xml
+++ b/enrollment-server/pom.xml
@@ -117,6 +117,12 @@
logstash-logback-encoder
+
+
+ io.micrometer
+ micrometer-registry-prometheus
+
+
org.springframework.boot
diff --git a/enrollment-server/src/main/resources/application.properties b/enrollment-server/src/main/resources/application.properties
index c16c6f2b3..a0da944e3 100644
--- a/enrollment-server/src/main/resources/application.properties
+++ b/enrollment-server/src/main/resources/application.properties
@@ -87,4 +87,10 @@ powerauth.service.correlation-header.enabled=false
powerauth.service.correlation-header.name=X-Correlation-ID
powerauth.service.correlation-header.value.validation-regexp=[a-zA-Z0-9\\-]{8,1024}
# For logging correlation HTTP headers enable the pattern and update correlation header name in the pattern
-#logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:%5p}) [%X{X-Correlation-ID}] %clr(%5p) %clr(${PID: }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}
\ No newline at end of file
+#logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:%5p}) [%X{X-Correlation-ID}] %clr(%5p) %clr(${PID: }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}
+
+# Monitoring
+#management.endpoint.metrics.enabled=true
+#management.endpoints.web.exposure.include=health, prometheus
+#management.endpoint.prometheus.enabled=true
+#management.prometheus.metrics.export.enabled=true
From ed83f97f1248626c8ad0b27aef073588cd6a267b Mon Sep 17 00:00:00 2001
From: Jan Dusil <134381434+jandusil@users.noreply.github.com>
Date: Thu, 21 Dec 2023 17:08:15 +0100
Subject: [PATCH 46/52] Fix #965: Reflect changes in filtering operations on
PowerAuth Server (#967)
* Fix #965: Reflect changes in filtering operations on PowerAuth Server
- Change activationFlags to activationId for filtering of requested operations
- Add test for MobileTokenService
---
.../controller/api/MobileTokenController.java | 8 +-
.../impl/service/MobileTokenService.java | 40 ++--
.../impl/service/MobileTokenServiceTest.java | 203 ++++++++++++++++++
3 files changed, 227 insertions(+), 24 deletions(-)
create mode 100644 enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/MobileTokenServiceTest.java
diff --git a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/controller/api/MobileTokenController.java b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/controller/api/MobileTokenController.java
index 3da69aba1..1ef71aeb8 100644
--- a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/controller/api/MobileTokenController.java
+++ b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/controller/api/MobileTokenController.java
@@ -106,9 +106,9 @@ public ObjectResponse operationList(@Parameter(hidden = t
if (auth != null) {
final String userId = auth.getUserId();
final String applicationId = auth.getApplicationId();
- final List activationFlags = auth.getActivationContext().getActivationFlags();
+ final String activationId = auth.getActivationContext().getActivationId();
final String language = locale.getLanguage();
- final OperationListResponse listResponse = mobileTokenService.operationListForUser(userId, applicationId, language, activationFlags, true);
+ final OperationListResponse listResponse = mobileTokenService.operationListForUser(userId, applicationId, language, activationId, true);
final Date currentTimestamp = new Date();
return new MobileTokenResponse<>(listResponse, currentTimestamp);
} else {
@@ -211,9 +211,9 @@ public ObjectResponse operationListAll(@Parameter(hidden
if (auth != null) {
final String userId = auth.getUserId();
final String applicationId = auth.getApplicationId();
- final List activationFlags = auth.getActivationContext().getActivationFlags();
+ final String activationId = auth.getActivationContext().getActivationId();
final String language = locale.getLanguage();
- final OperationListResponse listResponse = mobileTokenService.operationListForUser(userId, applicationId, language, activationFlags, false);
+ final OperationListResponse listResponse = mobileTokenService.operationListForUser(userId, applicationId, language, activationId, false);
return new ObjectResponse<>(listResponse);
} else {
throw new MobileTokenAuthException();
diff --git a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/MobileTokenService.java b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/MobileTokenService.java
index 5f27018c6..9b26f27cc 100644
--- a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/MobileTokenService.java
+++ b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/MobileTokenService.java
@@ -87,23 +87,25 @@ public MobileTokenService(PowerAuthClient powerAuthClient, MobileTokenConverter
}
/**
- * Get the operation list with operations of a given users. The service either returns only pending
- * operations or all operations, depending on the provided flag.
+ * Retrieves a list of operations for a specified user. This method can return
+ * either all operations or only those that are pending, based on the 'pendingOnly' flag.
+ * It processes each operation detail, converts them into a consistent format, and
+ * filters out operations without a corresponding template.
*
- * @param userId User ID.
- * @param applicationId Application ID.
- * @param language Language.
- * @param activationFlags Activation flags to condition the operation against.
- * @param pendingOnly Flag indicating if only pending or all operation should be returned.
- * @return Response with pending or all operations, depending on the "pendingOnly" flag.
- * @throws PowerAuthClientException In the case that PowerAuth service call fails.
- * @throws MobileTokenConfigurationException In the case of system misconfiguration.
+ * @param userId User ID for which the operation list is requested.
+ * @param applicationId Application ID associated with the operations.
+ * @param language Language for operation template localization.
+ * @param activationId Optional activation ID to filter operations.
+ * @param pendingOnly Flag indicating whether to fetch only pending operations or all.
+ * @return A consolidated list of operations formatted as 'OperationListResponse'.
+ * @throws PowerAuthClientException If there's an issue with the PowerAuth service call.
+ * @throws MobileTokenConfigurationException For any system configuration issues.
*/
public OperationListResponse operationListForUser(
@NotNull String userId,
@NotNull String applicationId,
@NotNull String language,
- List activationFlags,
+ String activationId,
boolean pendingOnly) throws PowerAuthClientException, MobileTokenConfigurationException {
final OperationListForUserRequest request = new OperationListForUserRequest();
@@ -111,6 +113,7 @@ public OperationListResponse operationListForUser(
request.setApplications(List.of(applicationId));
request.setPageNumber(0);
request.setPageSize(OPERATION_LIST_LIMIT);
+ request.setActivationId(activationId);
final MultiValueMap queryParams = httpCustomizationService.getQueryParams();
final MultiValueMap httpHeaders = httpCustomizationService.getHttpHeaders();
final com.wultra.security.powerauth.client.model.response.OperationListResponse operations =
@@ -120,16 +123,13 @@ public OperationListResponse operationListForUser(
final OperationListResponse responseObject = new OperationListResponse();
for (OperationDetailResponse operationDetail: operations) {
- final String activationFlag = operationDetail.getActivationFlag();
- if (activationFlag == null || activationFlags.contains(activationFlag)) { // only return data if there is no flag, or if flag matches flags of activation
- final Optional operationTemplate = operationTemplateService.findTemplate(operationDetail.getOperationType(), language);
- if (operationTemplate.isEmpty()) {
- logger.warn("No template found for operationType={}, skipping the entry.", operationDetail.getOperationType());
- continue;
- }
- final Operation operation = mobileTokenConverter.convert(operationDetail, operationTemplate.get());
- responseObject.add(operation);
+ final Optional operationTemplate = operationTemplateService.findTemplate(operationDetail.getOperationType(), language);
+ if (operationTemplate.isEmpty()) {
+ logger.warn("No template found for operationType={}, skipping the entry.", operationDetail.getOperationType());
+ continue;
}
+ final Operation operation = mobileTokenConverter.convert(operationDetail, operationTemplate.get());
+ responseObject.add(operation);
}
return responseObject;
}
diff --git a/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/MobileTokenServiceTest.java b/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/MobileTokenServiceTest.java
new file mode 100644
index 000000000..e125f7d38
--- /dev/null
+++ b/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/MobileTokenServiceTest.java
@@ -0,0 +1,203 @@
+/*
+ * PowerAuth Enrollment Server
+ * Copyright (C) 2023 Wultra s.r.o.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.wultra.app.enrollmentserver.impl.service;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import com.wultra.app.enrollmentserver.database.OperationTemplateRepository;
+import com.wultra.app.enrollmentserver.database.entity.OperationTemplateEntity;
+import com.wultra.app.enrollmentserver.impl.service.converter.MobileTokenConverter;
+import com.wultra.security.powerauth.client.PowerAuthClient;
+import com.wultra.security.powerauth.client.model.request.OperationListForUserRequest;
+import com.wultra.security.powerauth.client.model.response.OperationDetailResponse;
+import com.wultra.security.powerauth.lib.mtoken.model.entity.Operation;
+import com.wultra.security.powerauth.lib.mtoken.model.response.OperationListResponse;
+import io.getlime.security.powerauth.rest.api.spring.service.HttpCustomizationService;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.test.context.ActiveProfiles;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Test for {@link MobileTokenService}.
+ *
+ * @author Jan Dusil, jan.dusil@wultra.com
+ */
+@ExtendWith(MockitoExtension.class)
+@ActiveProfiles("test")
+class MobileTokenServiceTest {
+
+ @Mock
+ private PowerAuthClient powerAuthClient;
+
+ @Mock
+ private MobileTokenConverter mobileTokenConverter;
+
+ @Mock
+ private OperationTemplateService operationTemplateService;
+
+ @Mock
+ private HttpCustomizationService httpCustomizationService;
+
+ @InjectMocks
+ private MobileTokenService tested;
+
+ @Mock
+ private OperationTemplateRepository templateRepository;
+
+ private static final int OPERATION_LIST_LIMIT = 100;
+
+ @Test
+ void testOperationListForUser() throws Exception {
+ final String userId = "test-user";
+ final String applicationId = "21";
+ final String language = "CZ";
+ final String activationId = "test-activation";
+ final String operationType = "login";
+
+ final OperationListForUserRequest request = new OperationListForUserRequest();
+ request.setUserId(userId);
+ request.setApplications(List.of(applicationId));
+ request.setPageNumber(0);
+ request.setPageSize(OPERATION_LIST_LIMIT);
+ request.setActivationId(activationId);
+
+ final OperationDetailResponse operationDetailResponse = new OperationDetailResponse();
+ operationDetailResponse.setUserId(userId);
+ operationDetailResponse.setApplications(List.of(applicationId));
+ operationDetailResponse.setOperationType(operationType);
+ operationDetailResponse.setParameters(new HashMap<>());
+
+ final com.wultra.security.powerauth.client.model.response.OperationListResponse response
+ = new com.wultra.security.powerauth.client.model.response.OperationListResponse();
+ response.add(operationDetailResponse);
+
+ when(powerAuthClient.operationList(request, null, null)).thenReturn(response);
+
+ final OperationTemplateEntity operationTemplate = new OperationTemplateEntity();
+ operationTemplate.setLanguage(language);
+ operationTemplate.setPlaceholder(operationType);
+ operationTemplate.setId(1L);
+ when(operationTemplateService.findTemplate(operationType, language)).thenReturn(Optional.of(operationTemplate));
+
+ final Operation operation = new Operation();
+ operation.setName(operationType);
+ when(mobileTokenConverter.convert(operationDetailResponse, operationTemplate)).thenReturn(operation);
+
+ final OperationListResponse operationListResponse = tested.operationListForUser(userId, applicationId, language, activationId, false);
+
+ assertNotNull(operationListResponse);
+ assertEquals(1, operationListResponse.size());
+ assertNotNull(operationListResponse.get(0));
+ assertEquals(operationType, operationListResponse.get(0).getName());
+ }
+
+ @Test
+ void testPendingOperationListForUser() throws Exception {
+ final String userId = "test-user";
+ final String applicationId = "21";
+ final String language = "CZ";
+ final String activationId = "test-activation";
+ final String operationType = "login";
+
+ final OperationListForUserRequest request = new OperationListForUserRequest();
+ request.setUserId(userId);
+ request.setApplications(List.of(applicationId));
+ request.setPageNumber(0);
+ request.setPageSize(OPERATION_LIST_LIMIT);
+ request.setActivationId(activationId);
+
+ final OperationDetailResponse operationDetailResponse = new OperationDetailResponse();
+ operationDetailResponse.setUserId(userId);
+ operationDetailResponse.setApplications(List.of(applicationId));
+ operationDetailResponse.setOperationType(operationType);
+ operationDetailResponse.setParameters(new HashMap<>());
+
+ final com.wultra.security.powerauth.client.model.response.OperationListResponse response
+ = new com.wultra.security.powerauth.client.model.response.OperationListResponse();
+ response.add(operationDetailResponse);
+
+ when(powerAuthClient.operationPendingList(request, null, null)).thenReturn(response);
+
+ final OperationTemplateEntity operationTemplate = new OperationTemplateEntity();
+ operationTemplate.setLanguage(language);
+ operationTemplate.setPlaceholder(operationType);
+ operationTemplate.setId(1L);
+ when(operationTemplateService.findTemplate(operationType, language)).thenReturn(Optional.of(operationTemplate));
+
+ final Operation operation = new Operation();
+ operation.setName(operationType);
+ when(mobileTokenConverter.convert(operationDetailResponse, operationTemplate)).thenReturn(operation);
+
+ final OperationListResponse operationListResponse = tested.operationListForUser(userId, applicationId, language, activationId, true);
+
+ assertNotNull(operationListResponse);
+ assertEquals(1, operationListResponse.size());
+ assertNotNull(operationListResponse.get(0));
+ assertEquals(operationType, operationListResponse.get(0).getName());
+ }
+
+ @Test
+ void testOperationListForUserEmptyOperationTemplate() throws Exception {
+ final String userId = "test-user";
+ final String applicationId = "21";
+ final String language = "CZ";
+ final String activationId = "test-activation";
+ final String operationType = "login";
+
+ final OperationListForUserRequest request = new OperationListForUserRequest();
+ request.setUserId(userId);
+ request.setApplications(List.of(applicationId));
+ request.setPageNumber(0);
+ request.setPageSize(OPERATION_LIST_LIMIT);
+ request.setActivationId(activationId);
+
+ final OperationDetailResponse operationDetailResponse = new OperationDetailResponse();
+ operationDetailResponse.setUserId(userId);
+ operationDetailResponse.setApplications(List.of(applicationId));
+ operationDetailResponse.setOperationType(operationType);
+ operationDetailResponse.setParameters(new HashMap<>());
+
+ final com.wultra.security.powerauth.client.model.response.OperationListResponse response
+ = new com.wultra.security.powerauth.client.model.response.OperationListResponse();
+ response.add(operationDetailResponse);
+
+ when(powerAuthClient.operationList(request, null, null)).thenReturn(response);
+
+ final OperationTemplateEntity operationTemplate = new OperationTemplateEntity();
+ operationTemplate.setLanguage(language);
+ operationTemplate.setPlaceholder(operationType);
+ operationTemplate.setId(1L);
+
+ when(operationTemplateService.findTemplate(operationType, language)).thenReturn(Optional.empty());
+
+
+ final OperationListResponse operationListResponse = tested.operationListForUser(userId, applicationId, language, activationId, false);
+
+ assertNotNull(operationListResponse);
+ assertEquals(0, operationListResponse.size());
+ }
+
+}
From df26995a87900e293f40e6f5eac9747b2bb49c1e Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 25 Dec 2023 02:50:51 +0000
Subject: [PATCH 47/52] Bump org.openapitools:openapi-generator-maven-plugin
from 7.1.0 to 7.2.0
Bumps org.openapitools:openapi-generator-maven-plugin from 7.1.0 to 7.2.0.
---
updated-dependencies:
- dependency-name: org.openapitools:openapi-generator-maven-plugin
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
---
pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pom.xml b/pom.xml
index 6cd680b3d..79304f96b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -88,7 +88,7 @@
- 7.1.0
+ 7.2.0
5.10.2
4.0.0
From b11f7ab6628ceb4273fbb0b62353909cdd05ee5f Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 25 Dec 2023 02:51:46 +0000
Subject: [PATCH 48/52] Bump io.swagger.core.v3:swagger-annotations-jakarta
Bumps io.swagger.core.v3:swagger-annotations-jakarta from 2.2.19 to 2.2.20.
---
updated-dependencies:
- dependency-name: io.swagger.core.v3:swagger-annotations-jakarta
dependency-type: direct:production
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
---
pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pom.xml b/pom.xml
index 6cd680b3d..169923961 100644
--- a/pom.xml
+++ b/pom.xml
@@ -92,7 +92,7 @@
5.10.2
4.0.0
- 2.2.19
+ 2.2.20
2.3.0
1.4.2
From 4b58e1aa9405e26982ecfd1f0c7462c40d33adb7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Lubo=C5=A1=20Ra=C4=8Dansk=C3=BD?=
Date: Tue, 2 Jan 2024 13:45:29 +0100
Subject: [PATCH 49/52] Fix #970: AmountAttribute with invalid amount (#971)
* Fix #970: AmountAttribute with invalid amount
---
.../converter/MobileTokenConverter.java | 70 ++++-----
.../converter/MobileTokenConverterTest.java | 144 ++++++++++++++++++
2 files changed, 180 insertions(+), 34 deletions(-)
diff --git a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/converter/MobileTokenConverter.java b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/converter/MobileTokenConverter.java
index 27baae4d7..2bd4b8cf7 100644
--- a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/converter/MobileTokenConverter.java
+++ b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/converter/MobileTokenConverter.java
@@ -285,29 +285,39 @@ private static Optional buildAmountAttribute(final OperationTemplateP
if (currency.isEmpty()) {
return Optional.empty();
}
- final BigDecimal amountRaw;
- try {
- amountRaw = new BigDecimal(amount.get());
- } catch (NumberFormatException ex) {
- logger.warn("Invalid number format: {}, skipping the AMOUNT attribute!", amount);
- return Optional.empty();
- }
+
final Locale locale = LocaleContextHolder.getLocale();
final String currencyRaw = currency.get();
final String currencyFormatted = MonetaryConverter.formatCurrency(currencyRaw, locale);
- final String amountFormatted = MonetaryConverter.formatAmount(amountRaw, currencyRaw, locale);
- final String valueFormatted = MonetaryConverter.formatValue(amountRaw, currencyRaw, locale);
+ final AmountFormatted amountFormatted = createAmountFormatted(amount.get(), currencyRaw, "AMOUNT");
return Optional.of(AmountAttribute.builder()
.id(id)
.label(text)
- .amount(amountRaw)
- .amountFormatted(amountFormatted)
+ .amount(amountFormatted.amountRaw())
+ .amountFormatted(amountFormatted.amountFormatted())
.currency(currencyRaw)
.currencyFormatted(currencyFormatted)
- .valueFormatted(valueFormatted)
+ .valueFormatted(amountFormatted.valueFormatted())
.build());
}
+ private static AmountFormatted createAmountFormatted(final String amount, final String currencyRaw, final String attribute) {
+ final Locale locale = LocaleContextHolder.getLocale();
+
+ try {
+ final BigDecimal amountRaw = new BigDecimal(amount);
+ final String amountFormatted = MonetaryConverter.formatAmount(amountRaw, currencyRaw, locale);
+ final String valueFormatted = MonetaryConverter.formatValue(amountRaw, currencyRaw, locale);
+ return new AmountFormatted(amountRaw, amountFormatted, valueFormatted);
+ } catch (NumberFormatException e) {
+ logger.warn("Invalid number format: {}, the raw value is not filled in into {} attribute!", amount, attribute);
+ logger.trace("Invalid number format: {}, the raw value is not filled in into {} attribute!", amount, attribute, e);
+ // fallback - pass 'not a number' directly to the formatted field
+ final String valueFormatted = amount + " " + currencyRaw;
+ return new AmountFormatted(null, amount, valueFormatted);
+ }
+ }
+
private static Optional buildAmountConversionAttribute(final OperationTemplateParam templateParam, final Map params) {
final String id = templateParam.getId();
final String text = templateParam.getText();
@@ -326,39 +336,29 @@ private static Optional buildAmountConversionAttribute(final Operatio
.map(Boolean::parseBoolean)
.orElse(false);
- final BigDecimal sourceAmountRaw;
- final BigDecimal targetAmountRaw;
- try {
- sourceAmountRaw = new BigDecimal(sourceAmount.get());
- targetAmountRaw = new BigDecimal(targetAmount.get());
- } catch (NumberFormatException ex) {
- logger.warn("Invalid number format: {}, skipping the AMOUNT_CONVERSION attribute!", sourceAmount);
- return Optional.empty();
- }
-
final Locale locale = LocaleContextHolder.getLocale();
final String sourceCurrencyRaw = sourceCurrency.get();
- final String sourceCurrencyFormatted = MonetaryConverter.formatCurrency(sourceCurrencyRaw, locale);
- final String sourceAmountFormatted = MonetaryConverter.formatAmount(sourceAmountRaw, sourceCurrencyRaw, locale);
- final String sourceValueFormatted = MonetaryConverter.formatValue(sourceAmountRaw, sourceCurrencyRaw, locale);
+ final AmountFormatted sourceAmountFormatted = createAmountFormatted(sourceAmount.get(), sourceCurrencyRaw, "AMOUNT_CONVERSION");
+
final String targetCurrencyRaw = targetCurrency.get();
+ final AmountFormatted targetAmountFormatted = createAmountFormatted(targetAmount.get(), targetCurrencyRaw, "AMOUNT_CONVERSION");
+
+ final String sourceCurrencyFormatted = MonetaryConverter.formatCurrency(sourceCurrencyRaw, locale);
final String targetCurrencyFormatted = MonetaryConverter.formatCurrency(targetCurrencyRaw, locale);
- final String targetAmountFormatted = MonetaryConverter.formatAmount(targetAmountRaw, targetCurrencyRaw, locale);
- final String targetValueFormatted = MonetaryConverter.formatValue(targetAmountRaw, targetCurrencyRaw, locale);
return Optional.of(AmountConversionAttribute.builder()
.id(id)
.label(text)
.dynamic(dynamic)
- .sourceAmount(sourceAmountRaw)
- .sourceAmountFormatted(sourceAmountFormatted)
+ .sourceAmount(sourceAmountFormatted.amountRaw())
+ .sourceAmountFormatted(sourceAmountFormatted.amountFormatted())
.sourceCurrency(sourceCurrencyRaw)
.sourceCurrencyFormatted(sourceCurrencyFormatted)
- .sourceValueFormatted(sourceValueFormatted)
- .targetAmount(targetAmountRaw)
- .targetAmountFormatted(targetAmountFormatted)
+ .sourceValueFormatted(sourceAmountFormatted.valueFormatted())
+ .targetAmount(targetAmountFormatted.amountRaw())
+ .targetAmountFormatted(targetAmountFormatted.amountFormatted())
.targetCurrency(targetCurrencyRaw)
.targetCurrencyFormatted(targetCurrencyFormatted)
- .targetValueFormatted(targetValueFormatted)
+ .targetValueFormatted(targetAmountFormatted.valueFormatted())
.build());
}
@@ -397,7 +397,7 @@ private static Optional fetchTemplateParamValue(final OperationTemplateP
return Optional.empty();
}
if (params == null) {
- logger.warn("Params of OperationDetailResponse is null");
+ logger.warn("Params of OperationTemplateParam is null");
return Optional.empty();
}
return Optional.ofNullable(templateParams.get(key))
@@ -408,4 +408,6 @@ private static String fetchTemplateParamValueNullable(final OperationTemplatePar
return fetchTemplateParamValue(templateParam, params, key)
.orElse(null);
}
+
+ private record AmountFormatted(BigDecimal amountRaw, String amountFormatted, String valueFormatted) {}
}
diff --git a/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/converter/MobileTokenConverterTest.java b/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/converter/MobileTokenConverterTest.java
index b65d2f311..c17ce6611 100644
--- a/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/converter/MobileTokenConverterTest.java
+++ b/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/converter/MobileTokenConverterTest.java
@@ -515,6 +515,150 @@ void testConvertAttributes() throws Exception {
.build()), atributesIterator.next());
}
+ @Test
+ void testConvertAmount_notANumber() throws Exception {
+ final OperationDetailResponse operationDetail = createOperationDetailResponse();
+ operationDetail.setParameters(ImmutableMap.builder()
+ .put("amount", "not a number")
+ .put("currency", "CZK")
+ .build());
+
+ final OperationTemplateEntity operationTemplate = new OperationTemplateEntity();
+ operationTemplate.setAttributes("""
+ [
+ {
+ "id": "operation.amount",
+ "type": "AMOUNT",
+ "text": "Amount",
+ "params": {
+ "amount": "amount",
+ "currency": "currency"
+ }
+ }
+ ]""");
+
+ LocaleContextHolder.setLocale(new Locale("en"));
+ final Operation result = tested.convert(operationDetail, operationTemplate);
+
+ final List attributes = result.getFormData().getAttributes();
+
+ assertEquals(1, attributes.size());
+ final var atributesIterator = attributes.iterator();
+ assertEquals(AmountAttribute.builder()
+ .id("operation.amount")
+ .label("Amount")
+ .amount(null)
+ .amountFormatted("not a number")
+ .currency("CZK")
+ .currencyFormatted("CZK")
+ .valueFormatted("not a number CZK")
+ .build(), atributesIterator.next());
+ }
+
+ @Test
+ void testConvertAmountConversion_sourceNotANumber() throws Exception {
+ final OperationDetailResponse operationDetail = createOperationDetailResponse();
+ operationDetail.setParameters(ImmutableMap.builder()
+ .put("sourceAmount", "source not a number")
+ .put("sourceCurrency", "EUR")
+ .put("targetAmount", "1710.98")
+ .put("targetCurrency", "USD")
+ .put("dynamic", "true")
+ .build());
+
+ final OperationTemplateEntity operationTemplate = new OperationTemplateEntity();
+ operationTemplate.setAttributes("""
+ [
+ {
+ "id": "operation.amountConversion",
+ "type": "AMOUNT_CONVERSION",
+ "text": "Amount Conversion",
+ "params": {
+ "dynamic": "dynamic",
+ "sourceAmount": "sourceAmount",
+ "sourceCurrency": "sourceCurrency",
+ "targetAmount": "targetAmount",
+ "targetCurrency": "targetCurrency"
+ }
+ }
+ ]""");
+
+ LocaleContextHolder.setLocale(new Locale("en"));
+ final Operation result = tested.convert(operationDetail, operationTemplate);
+
+ final List attributes = result.getFormData().getAttributes();
+
+ assertEquals(1, attributes.size());
+ final var atributesIterator = attributes.iterator();
+ assertEquals(AmountConversionAttribute.builder()
+ .id("operation.amountConversion")
+ .label("Amount Conversion")
+ .dynamic(true)
+ .sourceAmount(null)
+ .sourceAmountFormatted("source not a number")
+ .sourceCurrency("EUR")
+ .sourceCurrencyFormatted("€")
+ .sourceValueFormatted("source not a number EUR")
+ .targetAmount(new BigDecimal("1710.98"))
+ .targetAmountFormatted("1,710.98")
+ .targetCurrency("USD")
+ .targetCurrencyFormatted("$")
+ .targetValueFormatted("$1,710.98")
+ .build(), atributesIterator.next());
+ }
+
+ @Test
+ void testConvertAmountConversion_targetNotANumber() throws Exception {
+ final OperationDetailResponse operationDetail = createOperationDetailResponse();
+ operationDetail.setParameters(ImmutableMap.builder()
+ .put("sourceAmount", "1710.98")
+ .put("sourceCurrency", "USD")
+ .put("targetAmount", "target not a number")
+ .put("targetCurrency", "EUR")
+ .put("dynamic", "true")
+ .build());
+
+ final OperationTemplateEntity operationTemplate = new OperationTemplateEntity();
+ operationTemplate.setAttributes("""
+ [
+ {
+ "id": "operation.amountConversion",
+ "type": "AMOUNT_CONVERSION",
+ "text": "Amount Conversion",
+ "params": {
+ "dynamic": "dynamic",
+ "sourceAmount": "sourceAmount",
+ "sourceCurrency": "sourceCurrency",
+ "targetAmount": "targetAmount",
+ "targetCurrency": "targetCurrency"
+ }
+ }
+ ]""");
+
+ LocaleContextHolder.setLocale(new Locale("en"));
+ final Operation result = tested.convert(operationDetail, operationTemplate);
+
+ final List attributes = result.getFormData().getAttributes();
+
+ assertEquals(1, attributes.size());
+ final var atributesIterator = attributes.iterator();
+ assertEquals(AmountConversionAttribute.builder()
+ .id("operation.amountConversion")
+ .label("Amount Conversion")
+ .dynamic(true)
+ .sourceAmount(new BigDecimal("1710.98"))
+ .sourceAmountFormatted("1,710.98")
+ .sourceCurrency("USD")
+ .sourceCurrencyFormatted("$")
+ .sourceValueFormatted("$1,710.98")
+ .targetAmount(null)
+ .targetAmountFormatted("target not a number")
+ .targetCurrency("EUR")
+ .targetCurrencyFormatted("€")
+ .targetValueFormatted("target not a number EUR")
+ .build(), atributesIterator.next());
+ }
+
@Test
void testConvertImageAttributeWithoutOriginalUrl() throws Exception {
final OperationDetailResponse operationDetail = createOperationDetailResponse();
From 452f27f9d8a52376b463bd2a41b84bbe01bd7a7d Mon Sep 17 00:00:00 2001
From: Lubos Racansky
Date: Mon, 8 Jan 2024 08:08:21 +0100
Subject: [PATCH 50/52] Fix #982: Update Docker dependencies
---
Dockerfile | 6 +++---
docs-private/Developer-How-To-Start.md | 8 ++++----
2 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/Dockerfile b/Dockerfile
index ca781b412..4f51ea7d4 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM ibm-semeru-runtimes:open-17.0.8_7-jre
+FROM ibm-semeru-runtimes:open-17.0.9_9-jre
LABEL maintainer="petr@wultra.com"
# Prepare environment variables
@@ -8,7 +8,7 @@ ENV JAVA_HOME=/opt/java/openjdk \
PKG_RELEASE=1~jammy \
TOMCAT_HOME=/usr/local/tomcat \
TOMCAT_MAJOR=10 \
- TOMCAT_VERSION=10.1.13 \
+ TOMCAT_VERSION=10.1.17 \
TZ=UTC
ENV PATH=$PATH:$LB_HOME:$TOMCAT_HOME/bin
@@ -20,7 +20,7 @@ RUN apt-get -y update \
# Install tomcat
RUN curl -jkSL -o /tmp/apache-tomcat.tar.gz http://archive.apache.org/dist/tomcat/tomcat-${TOMCAT_MAJOR}/v${TOMCAT_VERSION}/bin/apache-tomcat-${TOMCAT_VERSION}.tar.gz \
- && [ "406c0c367ac6ad95bb724ecc3a3c340ad7ded8c62287d657811eeec496eaaca1f5add52d2f46111da1426ae67090c543f6deccfeb5fdf4bdae32f9b39e773265 /tmp/apache-tomcat.tar.gz" = "$(sha512sum /tmp/apache-tomcat.tar.gz)" ] \
+ && [ "ff9670f9cd49a604e47edfbcfb5855fe59342048c3278ea8736276b51327adf2d076973f3ad1b8aa7870ef26c28cf7111527be810b445c9927f2a457795f5cb6 /tmp/apache-tomcat.tar.gz" = "$(sha512sum /tmp/apache-tomcat.tar.gz)" ] \
&& gunzip /tmp/apache-tomcat.tar.gz \
&& tar -C /opt -xf /tmp/apache-tomcat.tar \
&& ln -s /opt/apache-tomcat-$TOMCAT_VERSION $TOMCAT_HOME
diff --git a/docs-private/Developer-How-To-Start.md b/docs-private/Developer-How-To-Start.md
index 3a0eaad72..1ca53619d 100644
--- a/docs-private/Developer-How-To-Start.md
+++ b/docs-private/Developer-How-To-Start.md
@@ -36,20 +36,20 @@ mvn clean package
### Build the docker image
```shell
-docker build . -t enrollment-server:1.5.0
+docker build . -t enrollment-server:1.6.0
```
### Prepare environment variables
-* Copy `deploy/env.list.tmp` to `./env.list` and edit the values to use it via `docker run --env-file env.list IMAGE`
-* Or set environment variables via `docker run -e ENROLLMENT_SERVER_DATASOURCE_USERNAME='powerauth' IMAGE`
+* Copy `deploy/env.list.tmp` to `./env.list` and edit the values to use it via `docker run --env-file env.list enrollment-server:1.6.0`
+* Or set environment variables via `docker run -e ENROLLMENT_SERVER_DATASOURCE_USERNAME='powerauth' enrollment-server:1.6.0`
### Run the docker image
```shell
-docker run -p 80:8080 -e ENROLLMENT_SERVER_DATASOURCE_URL='jdbc:postgresql://host.docker.internal:5432/powerauth' -e ENROLLMENT_SERVER_DATASOURCE_USERNAME='powerauth' -e ENROLLMENT_SERVER_DATASOURCE_PASSWORD='' enrollment-server:1.5.0
+docker run -p 80:8080 -e ENROLLMENT_SERVER_DATASOURCE_URL='jdbc:postgresql://host.docker.internal:5432/powerauth' -e ENROLLMENT_SERVER_DATASOURCE_USERNAME='powerauth' -e ENROLLMENT_SERVER_DATASOURCE_PASSWORD='' enrollment-server:1.6.0
```
From ff2d2020de981b5379f56b25b482ab4690390502 Mon Sep 17 00:00:00 2001
From: Lubos Racansky
Date: Mon, 8 Jan 2024 08:54:03 +0100
Subject: [PATCH 51/52] Fix #984: Set release version to 1.6.0
---
enrollment-server-api-model/pom.xml | 2 +-
enrollment-server-onboarding-adapter-mock/pom.xml | 2 +-
enrollment-server-onboarding-api-model/pom.xml | 2 +-
enrollment-server-onboarding-api/pom.xml | 2 +-
enrollment-server-onboarding-common/pom.xml | 2 +-
enrollment-server-onboarding-domain-model/pom.xml | 2 +-
enrollment-server-onboarding-provider-innovatrics/pom.xml | 2 +-
enrollment-server-onboarding-provider-iproov/pom.xml | 2 +-
enrollment-server-onboarding-provider-zenid/pom.xml | 2 +-
enrollment-server-onboarding/pom.xml | 2 +-
enrollment-server/pom.xml | 2 +-
mtoken-model/pom.xml | 2 +-
pom.xml | 2 +-
13 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/enrollment-server-api-model/pom.xml b/enrollment-server-api-model/pom.xml
index a798a3719..d4fde669a 100644
--- a/enrollment-server-api-model/pom.xml
+++ b/enrollment-server-api-model/pom.xml
@@ -30,7 +30,7 @@
com.wultra.security
enrollment-server-parent
- 1.6.0-SNAPSHOT
+ 1.6.0
diff --git a/enrollment-server-onboarding-adapter-mock/pom.xml b/enrollment-server-onboarding-adapter-mock/pom.xml
index 99f5897a6..e3216d78d 100644
--- a/enrollment-server-onboarding-adapter-mock/pom.xml
+++ b/enrollment-server-onboarding-adapter-mock/pom.xml
@@ -24,7 +24,7 @@
com.wultra.security
enrollment-server-parent
- 1.6.0-SNAPSHOT
+ 1.6.0
enrollment-server-onboarding-adapter-mock
diff --git a/enrollment-server-onboarding-api-model/pom.xml b/enrollment-server-onboarding-api-model/pom.xml
index b22e220c9..aea8d0ba1 100644
--- a/enrollment-server-onboarding-api-model/pom.xml
+++ b/enrollment-server-onboarding-api-model/pom.xml
@@ -7,7 +7,7 @@
com.wultra.security
enrollment-server-parent
- 1.6.0-SNAPSHOT
+ 1.6.0
enrollment-server-onboarding-api-model
diff --git a/enrollment-server-onboarding-api/pom.xml b/enrollment-server-onboarding-api/pom.xml
index 48cb44c2e..82203d276 100644
--- a/enrollment-server-onboarding-api/pom.xml
+++ b/enrollment-server-onboarding-api/pom.xml
@@ -25,7 +25,7 @@
com.wultra.security
enrollment-server-parent
- 1.6.0-SNAPSHOT
+ 1.6.0
com.wultra.security
diff --git a/enrollment-server-onboarding-common/pom.xml b/enrollment-server-onboarding-common/pom.xml
index d6d88b13e..7c54ec298 100644
--- a/enrollment-server-onboarding-common/pom.xml
+++ b/enrollment-server-onboarding-common/pom.xml
@@ -24,7 +24,7 @@
com.wultra.security
enrollment-server-parent
- 1.6.0-SNAPSHOT
+ 1.6.0
enrollment-server-onboarding-common
diff --git a/enrollment-server-onboarding-domain-model/pom.xml b/enrollment-server-onboarding-domain-model/pom.xml
index 05d934ce1..cc0fb0b21 100644
--- a/enrollment-server-onboarding-domain-model/pom.xml
+++ b/enrollment-server-onboarding-domain-model/pom.xml
@@ -30,7 +30,7 @@
com.wultra.security
enrollment-server-parent
- 1.6.0-SNAPSHOT
+ 1.6.0
diff --git a/enrollment-server-onboarding-provider-innovatrics/pom.xml b/enrollment-server-onboarding-provider-innovatrics/pom.xml
index bec000cb1..1fd6417a4 100644
--- a/enrollment-server-onboarding-provider-innovatrics/pom.xml
+++ b/enrollment-server-onboarding-provider-innovatrics/pom.xml
@@ -25,7 +25,7 @@
com.wultra.security
enrollment-server-parent
- 1.6.0-SNAPSHOT
+ 1.6.0
com.wultra.security
diff --git a/enrollment-server-onboarding-provider-iproov/pom.xml b/enrollment-server-onboarding-provider-iproov/pom.xml
index 3822d8b36..505cfa31c 100644
--- a/enrollment-server-onboarding-provider-iproov/pom.xml
+++ b/enrollment-server-onboarding-provider-iproov/pom.xml
@@ -25,7 +25,7 @@
com.wultra.security
enrollment-server-parent
- 1.6.0-SNAPSHOT
+ 1.6.0
com.wultra.security
diff --git a/enrollment-server-onboarding-provider-zenid/pom.xml b/enrollment-server-onboarding-provider-zenid/pom.xml
index 00c1a27ca..e6b8b938a 100644
--- a/enrollment-server-onboarding-provider-zenid/pom.xml
+++ b/enrollment-server-onboarding-provider-zenid/pom.xml
@@ -25,7 +25,7 @@
com.wultra.security
enrollment-server-parent
- 1.6.0-SNAPSHOT
+ 1.6.0
com.wultra.security
diff --git a/enrollment-server-onboarding/pom.xml b/enrollment-server-onboarding/pom.xml
index 3c9f79bb3..e3f495278 100644
--- a/enrollment-server-onboarding/pom.xml
+++ b/enrollment-server-onboarding/pom.xml
@@ -29,7 +29,7 @@
com.wultra.security
enrollment-server-parent
- 1.6.0-SNAPSHOT
+ 1.6.0
diff --git a/enrollment-server/pom.xml b/enrollment-server/pom.xml
index bd0e7e310..bea22f846 100644
--- a/enrollment-server/pom.xml
+++ b/enrollment-server/pom.xml
@@ -30,7 +30,7 @@
com.wultra.security
enrollment-server-parent
- 1.6.0-SNAPSHOT
+ 1.6.0
diff --git a/mtoken-model/pom.xml b/mtoken-model/pom.xml
index a95dd7844..880febd13 100644
--- a/mtoken-model/pom.xml
+++ b/mtoken-model/pom.xml
@@ -26,7 +26,7 @@
com.wultra.security
enrollment-server-parent
- 1.6.0-SNAPSHOT
+ 1.6.0
diff --git a/pom.xml b/pom.xml
index 0cde978f1..50ea871fe 100644
--- a/pom.xml
+++ b/pom.xml
@@ -26,7 +26,7 @@
com.wultra.security
enrollment-server-parent
- 1.6.0-SNAPSHOT
+ 1.6.0
pom
From 41b9ea14eab4c436e57dd9c7b56f6bb6fb600060 Mon Sep 17 00:00:00 2001
From: Lubos Racansky
Date: Tue, 9 Jan 2024 07:11:56 +0100
Subject: [PATCH 52/52] Remove commented out code
Follow-up to #907
---
.../provider/innovatrics/InnovatricsApiService.java | 8 --------
1 file changed, 8 deletions(-)
diff --git a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsApiService.java b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsApiService.java
index 916c08fb7..bc3caebde 100644
--- a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsApiService.java
+++ b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsApiService.java
@@ -222,14 +222,6 @@ public CreateSelfieResponse createSelfie(final String customerId, final String l
}
}
- // TODO remove - temporal test call
-// @PostConstruct
-// public void testCall() throws RestClientException {
-// logger.info("Trying a test call");
-// final ResponseEntity response = restClient.get("/api/v1/metadata", STRING_TYPE_REFERENCE);
-// logger.info("Result of test call: {}", response.getBody());
-// }
-
/**
* Create a new customer resource.
* @param ownerId owner identification.