Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

KYC Service Layer #35774

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
38 changes: 36 additions & 2 deletions corehq/apps/integration/kyc/models.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from django.conf import settings
from django.db import models
from django.utils.translation import gettext as _

import jsonfield

from corehq.motech.const import OAUTH2_CLIENT
from corehq.motech.models import ConnectionSettings


Expand All @@ -18,6 +20,9 @@ class UserDataStore(object):


class KycProviders(models.TextChoices):
# When adding a new provider:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to have a short documentation for steps to add a new provider.
Could be done in the follow up work along with TODOs.

# 1. Add connection settings to `settings.py` if necessary
# 2. Add it to `KycConfig.get_connections_settings()`
MTN_KYC = 'mtn_kyc', _('MTN KYC')


Expand All @@ -26,10 +31,39 @@ class KycConfig(models.Model):
user_data_store = models.CharField(max_length=25, choices=UserDataStore.CHOICES)
other_case_type = models.CharField(max_length=126, null=True)
api_field_to_user_data_map = jsonfield.JSONField(default=list)
connection_settings = models.ForeignKey(ConnectionSettings, on_delete=models.PROTECT)
provider = models.CharField(max_length=25, choices=KycProviders.choices, default=KycProviders.MTN_KYC)
provider = models.CharField(
max_length=25,
choices=KycProviders.choices,
default=KycProviders.MTN_KYC,
)
connection_settings = models.ForeignKey(
ConnectionSettings,
on_delete=models.PROTECT,
# Assumes we can determine connection settings for provider
null=True,
blank=True,
)

class Meta:
constraints = [
models.UniqueConstraint(fields=['domain', 'provider'], name='unique_domain_provider'),
]

def get_connection_settings(self):
if not self.connection_settings_id:
if self.provider == KycProviders.MTN_KYC:
kyc_settings = settings.MTN_KYC_CONNECTION_SETTINGS
self.connection_settings = ConnectionSettings.objects.create(
domain=self.domain,
name=KycProviders.MTN_KYC.label,
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'],
)
self.save()
# elif self.provider == KycProviders.NEW_PROVIDER_HERE: ...
else:
raise ValueError(f'Unable to determine connection settings for KYC provider {self.provider!r}.')
return self.connection_settings
49 changes: 49 additions & 0 deletions corehq/apps/integration/kyc/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from django.test import TestCase

import pytest

from corehq.apps.integration.kyc.models import KycConfig, UserDataStore
from corehq.motech.models import ConnectionSettings

DOMAIN = 'test-domain'


class TestGetConnectionSettings(TestCase):

def test_valid_without_connection_settings(self):
config = KycConfig(
domain=DOMAIN,
user_data_store=UserDataStore.USER_CASE,
)
# Does not raise `django.db.utils.IntegrityError`
config.save()

def test_get_connection_settings(self):
config = KycConfig(
domain=DOMAIN,
user_data_store=UserDataStore.USER_CASE,
)
assert ConnectionSettings.objects.count() == 0

connx = config.get_connection_settings()
assert isinstance(connx, ConnectionSettings)
# First call creates ConnectionSettings
assert ConnectionSettings.objects.count() == 1

connx = config.get_connection_settings()
assert isinstance(connx, ConnectionSettings)
# Subsequent calls get existing ConnectionSettings
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,
match="^Unable to determine connection settings for KYC provider "
"'invalid'.$",
):
config.get_connection_settings()
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 4.2.18 on 2025-02-16 22:53

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
("motech", "0017_connectionsettings_use_aes_cbc_encryption"),
("integration", "0008_kycconfig_provider_kycconfig_unique_domain_provider"),
]

operations = [
migrations.AlterField(
model_name="kycconfig",
name="connection_settings",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="motech.connectionsettings",
),
),
]
1 change: 1 addition & 0 deletions migrations.lock
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,7 @@ integration
0006_kycconfig
0007_alter_kycconfig_api_field_to_user_data_map
0008_kycconfig_provider_kycconfig_unique_domain_provider
0009_alter_kycconfig_connection_settings
ivr
0001_initial
0002_call_app_id
Expand Down
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
MTN_KYC_CONNECTION_SETTINGS = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I am probably behind on recent updates with MTN )
What is the rationale for having a common connection settings ? With this not be project specific ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It turns out that the API service provider will have a contract with the platform provider (i.e. Dimagi), and will grant access to them, and not to each project ...

... which has just made me think that saving the ConnectionSettings instance is not a good idea. Projects/users can't read the secrets of ConnectionSettings instances, but they can change them. So either we need to have a way to write-protect ConnectioinSettings, or we need to create them on the fly without persisting them.

'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