-
-
Notifications
You must be signed in to change notification settings - Fork 222
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
278 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
{ | ||
"$schema": "http://json-schema.org/draft-07/schema#", | ||
"type": "object", | ||
"required": [ | ||
"firstName", | ||
"lastName", | ||
"phoneNumber", | ||
"emailAddress", | ||
"nationalIdNumber", | ||
"streetAddress", | ||
"city", | ||
"postCode", | ||
"country" | ||
], | ||
"properties": { | ||
"firstName": { | ||
"type": "string" | ||
}, | ||
"lastName": { | ||
"type": "string" | ||
}, | ||
"phoneNumber": { | ||
"type": "string" | ||
}, | ||
"emailAddress": { | ||
"type": "string" | ||
}, | ||
"nationalIdNumber": { | ||
"type": "string" | ||
}, | ||
"streetAddress": { | ||
"type": "string" | ||
}, | ||
"city": { | ||
"type": "string" | ||
}, | ||
"postCode": { | ||
"type": "string" | ||
}, | ||
"country": { | ||
"type": "string" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,132 @@ | ||
from corehq.apps.integration.kyc.models import UserDataStore | ||
import json | ||
import os | ||
import re | ||
|
||
import jsonschema | ||
from django.conf import settings | ||
from django.utils.text import camel_case_to_spaces | ||
|
||
from corehq.apps.integration.kyc.models import ( | ||
KycConfig, | ||
KycProviders, | ||
UserDataStore, | ||
) | ||
from corehq.motech.const import OAUTH2_CLIENT | ||
from corehq.motech.models import ConnectionSettings | ||
|
||
CONNECTION_SETTINGS_NAME = 'Chenosis MTN KYC' | ||
|
||
|
||
class UserCaseNotFound(Exception): | ||
pass | ||
|
||
|
||
def verify_user(domain, source): | ||
""" | ||
Verify a user using the Chenosis MTN KYC API. | ||
Returns a tuple of ``(verified_okay, bad_fields)`` where | ||
``verified_okay`` is a boolean indicating whether the user was | ||
verified successfully, and ``bad_fields`` is a list of fields that | ||
failed verification. | ||
""" | ||
# Documentation: https://apiportal.chenosis.io/en/apis/kyc-verify#endpoints | ||
# | ||
# Example request: | ||
# | ||
# POST https://dev.api.chenosis.io/kycVerify/v1/customers/{customerId} | ||
# { | ||
# "firstName": "John", | ||
# "lastName": "Doe", | ||
# "phoneNumber": "27650600900", | ||
# "emailAddress": "[email protected]", | ||
# "nationalIdNumber": "123456789", | ||
# "streetAddress": "1st Park Avenue, Mzansi, Johannesburg", | ||
# "city": "Johannesburg", | ||
# "postCode": "20200", | ||
# "country": "South Africa" | ||
# } | ||
# | ||
# Example 200 response: | ||
# | ||
# { | ||
# "statusCode": "0000", | ||
# "statusMessage": "Success.", | ||
# "customerId": "27650600900", | ||
# "transactionId": "232TXYZ-212", | ||
# "data": { | ||
# "firstName": 100, | ||
# "lastName": 100, | ||
# "phoneNumber": 100, | ||
# "emailAddress": 100, | ||
# "nationalIdNumber": 100, | ||
# "streetAddress": 100, | ||
# "city": 100, | ||
# "postCode": 100, | ||
# "country": 100 | ||
# } | ||
# } | ||
|
||
# TODO: Determine what thresholds we want | ||
success_thresholds = { | ||
'firstName': 100, | ||
'lastName': 100, | ||
'phoneNumber': 100, | ||
'emailAddress': 100, | ||
'nationalIdNumber': 100, | ||
'streetAddress': 80, # Allow streetAddress to be less accurate | ||
'city': 100, | ||
'postCode': 100, | ||
'country': 0, # e.g "Ivory Coast" vs. "Republic of Côte d'Ivoire" vs. "CIV"? | ||
} | ||
|
||
kyc_config = KycConfig.objects.get(domain=domain) | ||
user_data = get_user_data_for_api(source, kyc_config) | ||
_validate_schema('kycVerify/v1', user_data) | ||
requests = _get_requests(domain, kyc_config) | ||
response = requests.post( | ||
f'/kycVerify/v1/customers/{user_data["phoneNumber"]}', | ||
json=user_data, | ||
) | ||
response.raise_for_status() | ||
# TODO: Is this what we want to return? | ||
field_scores = response.json().get('data', {}) | ||
bad_fields = [k for k, v in field_scores if v < success_thresholds[k]] | ||
return bool(field_scores and not bad_fields), bad_fields | ||
|
||
|
||
def verify_users(domain, source_list): | ||
# TODO: An endpoint to verify a group of users does not seem to be | ||
# available using Chenosis | ||
results = [] | ||
for source in source_list: | ||
result = verify_user(domain, source) | ||
results.append(result) | ||
return results | ||
|
||
|
||
def _get_requests(domain, config): | ||
""" | ||
Returns a `Requests` instance for the Chenosis MTN KYC API | ||
""" | ||
if config.provider == KycProviders.MTN_KYC: | ||
kyc_settings = settings.KYC_MTN_CONNECTION_SETTINGS | ||
connx, __ = ConnectionSettings.objects.get_or_create( | ||
domain=domain, | ||
name=CONNECTION_SETTINGS_NAME, | ||
defaults={ | ||
'url': kyc_settings['url'], | ||
'auth_type': OAUTH2_CLIENT, | ||
'client_id': kyc_settings['client_id'], | ||
'client_secret': kyc_settings['client_secret'], | ||
'token_url': kyc_settings['token_url'], | ||
}, | ||
) | ||
else: | ||
raise ValueError(f'KYC provider {config.provider!r} not supported') | ||
return connx.get_requests() | ||
|
||
|
||
def get_user_data_for_api(source, config): | ||
""" | ||
Returns a dictionary of user data for the API. | ||
|
@@ -39,3 +161,29 @@ def _get_source_data(source, config): | |
else: | ||
source_data = source.case_json | ||
return source_data | ||
|
||
|
||
def _validate_schema(endpoint, data): | ||
""" | ||
Validate the data against the schema for the given endpoint | ||
""" | ||
schema_filename = f'{_kebab_case(endpoint)}.json' | ||
schema_path = os.path.join( | ||
settings.BASE_DIR, 'corehq', 'apps', 'integration', 'kyc', 'schemas', | ||
schema_filename, | ||
) | ||
with open(schema_path) as f: | ||
schema = json.load(f) | ||
jsonschema.validate(data, schema) | ||
|
||
|
||
def _kebab_case(value): | ||
""" | ||
Convert a string to kebab-case | ||
>>> _kebab_case('Hello, World!') | ||
'hello-world' | ||
""" | ||
value = camel_case_to_spaces(value) | ||
value = re.sub(r"[^\w\s-]", "-", value.lower()) | ||
return re.sub(r"[-\s]+", "-", value).strip("-_") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
from django.test import TestCase | ||
|
||
from corehq.apps.integration.kyc.models import KycConfig, UserDataStore | ||
from corehq.apps.integration.kyc.services import _get_requests | ||
import doctest | ||
|
||
import jsonschema | ||
import pytest | ||
|
||
from corehq.apps.integration.kyc.services import _validate_schema | ||
from corehq.motech.models import ConnectionSettings | ||
from corehq.motech.requests import Requests | ||
|
||
DOMAIN = 'test-domain' | ||
|
||
|
||
def test_doctests(): | ||
import corehq.apps.integration.kyc.services as module | ||
results = doctest.testmod(module) | ||
assert results.failed == 0 | ||
|
||
|
||
class TestValidateSchema(TestCase): | ||
|
||
def test_valid_schema(self): | ||
endpoint = 'kycVerify/v1' | ||
data = { | ||
'firstName': 'John', | ||
'lastName': 'Doe', | ||
'phoneNumber': '27650600900', | ||
'emailAddress': '[email protected]', | ||
'nationalIdNumber': '123456789', | ||
'streetAddress': '1st Park Avenue, Mzansi, Johannesburg', | ||
'city': 'Johannesburg', | ||
'postCode': '20200', | ||
'country': 'South Africa', | ||
} | ||
_validate_schema(endpoint, data) # Should not raise an exception | ||
|
||
def test_invalid_schema(self): | ||
endpoint = 'kycVerify/v1' | ||
data = { | ||
'firstName': 'John', | ||
'lastName': 'Doe', | ||
# Missing required fields | ||
} | ||
with pytest.raises(jsonschema.ValidationError): | ||
_validate_schema(endpoint, data) | ||
|
||
|
||
class TestGetRequests(TestCase): | ||
|
||
def test_get_requests(self): | ||
config = KycConfig( | ||
domain=DOMAIN, | ||
user_data_store=UserDataStore.USER_CASE, | ||
) | ||
assert ConnectionSettings.objects.count() == 0 | ||
|
||
# Creates ConnectionSettings | ||
requests = _get_requests(DOMAIN, config) | ||
assert isinstance(requests, Requests) | ||
assert ConnectionSettings.objects.count() == 1 | ||
|
||
# Gets existing ConnectionSettings | ||
requests = _get_requests(DOMAIN, config) | ||
assert isinstance(requests, Requests) | ||
assert ConnectionSettings.objects.count() == 1 | ||
|
||
def test_bad_config(self): | ||
config = KycConfig( | ||
domain=DOMAIN, | ||
user_data_store=UserDataStore.USER_CASE, | ||
provider='invalid', | ||
) | ||
with pytest.raises(ValueError): | ||
_get_requests(DOMAIN, config) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters