Skip to content

Commit

Permalink
Add services to call KYC API
Browse files Browse the repository at this point in the history
  • Loading branch information
kaapstorm committed Feb 13, 2025
1 parent 5ed4c59 commit da063e7
Show file tree
Hide file tree
Showing 4 changed files with 278 additions and 1 deletion.
44 changes: 44 additions & 0 deletions corehq/apps/integration/kyc/schemas/kyc-verify-v1.json
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"
}
}
}
150 changes: 149 additions & 1 deletion corehq/apps/integration/kyc/services.py
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.
Expand Down Expand Up @@ -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("-_")
77 changes: 77 additions & 0 deletions corehq/apps/integration/kyc/tests/test_services.py
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)
8 changes: 8 additions & 0 deletions settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -1172,6 +1172,14 @@ def _pkce_required(client_id):
# used by periodic tasks that delete soft deleted data older than PERMANENT_DELETION_WINDOW days
PERMANENT_DELETION_WINDOW = 30 # days

# Used by `corehq.apps.integration.kyc`. Override in localsettings.py
KYC_MTN_CONNECTION_SETTINGS = {
'url': 'https://dev.api.chenosis.io/',
'token_url': 'https://dev.api.chenosis.io/oauth/client/accesstoken',
'client_id': 'test',
'client_secret': 'password',
}


try:
# try to see if there's an environmental variable set for local_settings
Expand Down

0 comments on commit da063e7

Please sign in to comment.