feat: implement e-Aadhaar KYC upload and verification service using Surepass API
Some checks failed
Deploy Beta (NATIVE) / deploy (push) Failing after 3s
Some checks failed
Deploy Beta (NATIVE) / deploy (push) Failing after 3s
This commit is contained in:
parent
d27643cf05
commit
b49eb17027
7 changed files with 225 additions and 2 deletions
31
api/migrations/0006_sellerkyc.py
Normal file
31
api/migrations/0006_sellerkyc.py
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Generated by Django 6.1 on 2026-08-28 14:22
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('api', '0005_product_additional_images_product_compare_at_price_and_more'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SellerKYC',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('aadhaar_number', models.CharField(blank=True, max_length=20, null=True)),
|
||||||
|
('name', models.CharField(blank=True, max_length=255, null=True)),
|
||||||
|
('dob', models.CharField(blank=True, max_length=50, null=True)),
|
||||||
|
('gender', models.CharField(blank=True, max_length=10, null=True)),
|
||||||
|
('address', models.TextField(blank=True, null=True)),
|
||||||
|
('face_base64', models.TextField(blank=True, null=True)),
|
||||||
|
('verification_response', models.JSONField(blank=True, default=dict, null=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('supplier', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='kyc', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -61,6 +61,20 @@ class SupplierProfile(models.Model):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.user.username}'s Profile"
|
return f"{self.user.username}'s Profile"
|
||||||
|
|
||||||
|
class SellerKYC(models.Model):
|
||||||
|
supplier = models.OneToOneField(User, on_delete=models.CASCADE, related_name='kyc')
|
||||||
|
aadhaar_number = models.CharField(max_length=20, blank=True, null=True)
|
||||||
|
name = models.CharField(max_length=255, blank=True, null=True)
|
||||||
|
dob = models.CharField(max_length=50, blank=True, null=True)
|
||||||
|
gender = models.CharField(max_length=10, blank=True, null=True)
|
||||||
|
address = models.TextField(blank=True, null=True)
|
||||||
|
face_base64 = models.TextField(blank=True, null=True)
|
||||||
|
verification_response = models.JSONField(default=dict, blank=True, null=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.supplier.username} - KYC"
|
||||||
|
|
||||||
class OTPRecord(models.Model):
|
class OTPRecord(models.Model):
|
||||||
TYPE_CHOICES = [
|
TYPE_CHOICES = [
|
||||||
('phone', 'Phone'),
|
('phone', 'Phone'),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import re
|
import re
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from .models import SupplierProfile, Product, Order, ReturnRequest, Wallet, WalletTransaction, Category
|
from .models import SupplierProfile, Product, Order, ReturnRequest, Wallet, WalletTransaction, Category, SellerKYC
|
||||||
|
|
||||||
class CategoryListField(serializers.RelatedField):
|
class CategoryListField(serializers.RelatedField):
|
||||||
def to_representation(self, value):
|
def to_representation(self, value):
|
||||||
|
|
@ -11,17 +11,28 @@ class CategoryListField(serializers.RelatedField):
|
||||||
category, _ = Category.objects.get_or_create(name=data)
|
category, _ = Category.objects.get_or_create(name=data)
|
||||||
return category
|
return category
|
||||||
|
|
||||||
|
class SellerKYCSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = SellerKYC
|
||||||
|
fields = ('aadhaar_number', 'name', 'dob', 'gender', 'address', 'created_at')
|
||||||
|
|
||||||
class SupplierProfileSerializer(serializers.ModelSerializer):
|
class SupplierProfileSerializer(serializers.ModelSerializer):
|
||||||
onboarding_step = serializers.SerializerMethodField()
|
onboarding_step = serializers.SerializerMethodField()
|
||||||
categories = CategoryListField(many=True, queryset=Category.objects.all(), required=False)
|
categories = CategoryListField(many=True, queryset=Category.objects.all(), required=False)
|
||||||
aadhar_url = serializers.SerializerMethodField()
|
aadhar_url = serializers.SerializerMethodField()
|
||||||
pan_url = serializers.SerializerMethodField()
|
pan_url = serializers.SerializerMethodField()
|
||||||
logo_url = serializers.SerializerMethodField()
|
logo_url = serializers.SerializerMethodField()
|
||||||
|
kyc = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SupplierProfile
|
model = SupplierProfile
|
||||||
exclude = ('user',)
|
exclude = ('user',)
|
||||||
|
|
||||||
|
def get_kyc(self, obj):
|
||||||
|
if hasattr(obj.user, 'kyc'):
|
||||||
|
return SellerKYCSerializer(obj.user.kyc).data
|
||||||
|
return None
|
||||||
|
|
||||||
def get_aadhar_url(self, obj):
|
def get_aadhar_url(self, obj):
|
||||||
from .views import get_document_url
|
from .views import get_document_url
|
||||||
request = self.context.get('request')
|
request = self.context.get('request')
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ from rest_framework_simplejwt.views import TokenRefreshView
|
||||||
from .views import (
|
from .views import (
|
||||||
RegisterView, LoginView, LogoutView, VerifyOtpView, ProfileView,
|
RegisterView, LoginView, LogoutView, VerifyOtpView, ProfileView,
|
||||||
ProductViewSet, BulkUploadView, OrderViewSet, ReturnRequestViewSet,
|
ProductViewSet, BulkUploadView, OrderViewSet, ReturnRequestViewSet,
|
||||||
WalletView, WithdrawView, SubmitGstinView, PresignedUrlView, MockUploadView, MockDownloadView
|
WalletView, WithdrawView, SubmitGstinView, PresignedUrlView, MockUploadView, MockDownloadView,
|
||||||
|
UploadEAadhaarView
|
||||||
)
|
)
|
||||||
|
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
|
|
@ -23,6 +24,7 @@ urlpatterns = [
|
||||||
path('profile/presigned-url/', PresignedUrlView.as_view(), name='presigned-url'),
|
path('profile/presigned-url/', PresignedUrlView.as_view(), name='presigned-url'),
|
||||||
path('profile/mock-upload/', MockUploadView.as_view(), name='mock-upload'),
|
path('profile/mock-upload/', MockUploadView.as_view(), name='mock-upload'),
|
||||||
path('profile/mock-download/', MockDownloadView.as_view(), name='mock-download'),
|
path('profile/mock-download/', MockDownloadView.as_view(), name='mock-download'),
|
||||||
|
path('profile/kyc/upload-eaadhaar/', UploadEAadhaarView.as_view(), name='kyc_upload_eaadhaar'),
|
||||||
path('products/bulk-upload/', BulkUploadView.as_view(), name='bulk-upload'),
|
path('products/bulk-upload/', BulkUploadView.as_view(), name='bulk-upload'),
|
||||||
path('wallet/', WalletView.as_view(), name='wallet'),
|
path('wallet/', WalletView.as_view(), name='wallet'),
|
||||||
path('wallet/withdraw/', WithdrawView.as_view(), name='withdraw'),
|
path('wallet/withdraw/', WithdrawView.as_view(), name='withdraw'),
|
||||||
|
|
|
||||||
145
api/views.py
145
api/views.py
|
|
@ -597,3 +597,148 @@ class WithdrawView(APIView):
|
||||||
|
|
||||||
logger.info('Withdraw successful: user=%s amount=%.2f tx_id=%s', user.username, amount, tx.tx_id)
|
logger.info('Withdraw successful: user=%s amount=%.2f tx_id=%s', user.username, amount, tx.tx_id)
|
||||||
return Response(WalletSerializer(wallet).data)
|
return Response(WalletSerializer(wallet).data)
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from django.core.mail import send_mail
|
||||||
|
from rest_framework.parsers import MultiPartParser, FormParser
|
||||||
|
from .models import SellerKYC
|
||||||
|
|
||||||
|
class UploadEAadhaarView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
parser_classes = [MultiPartParser, FormParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
file_obj = request.FILES.get('file')
|
||||||
|
if not file_obj:
|
||||||
|
return Response({'error': 'file is required'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
user = request.user
|
||||||
|
profile, _ = SupplierProfile.objects.get_or_create(user=user)
|
||||||
|
|
||||||
|
# 1. Store the file locally (mock S3) or to S3 directly
|
||||||
|
ext = 'pdf'
|
||||||
|
if file_obj.name.endswith('.xml'):
|
||||||
|
ext = 'xml'
|
||||||
|
version_name = f"version_{secrets.token_hex(4)}"
|
||||||
|
s3_key = f"supplierdocument/{user.id}/eaadhaar/{version_name}.{ext}"
|
||||||
|
|
||||||
|
aws_access = os.environ.get('AWS_ACCESS_KEY_ID') or os.environ.get('AWS_ACCESS_KEY')
|
||||||
|
aws_secret = os.environ.get('AWS_SECRET_ACCESS_KEY') or os.environ.get('AWS_SECRET_KEY')
|
||||||
|
bucket_name = os.environ.get('AWS_STORAGE_BUCKET_NAME') or os.environ.get('AWS_BUCKET_NAME') or 'betasupplierdocumentstorage'
|
||||||
|
region_name = os.environ.get('AWS_REGION', 'ap-south-2')
|
||||||
|
use_s3 = getattr(settings, 'USE_S3', False) or (aws_access and aws_secret)
|
||||||
|
|
||||||
|
if use_s3:
|
||||||
|
try:
|
||||||
|
import boto3
|
||||||
|
s3_client = boto3.client(
|
||||||
|
's3',
|
||||||
|
region_name=region_name,
|
||||||
|
aws_access_key_id=aws_access,
|
||||||
|
aws_secret_access_key=aws_secret
|
||||||
|
)
|
||||||
|
s3_client.upload_fileobj(file_obj, bucket_name, s3_key)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"S3 upload failed: {e}")
|
||||||
|
return Response({'error': 'Failed to upload document to storage.'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
else:
|
||||||
|
base_dir = os.environ.get('UPLOAD_MOUNT_DIR', '/home/ubuntu/mnt/s3files/supplierdocuments')
|
||||||
|
if not os.path.exists('/home/ubuntu/mnt/s3files/supplierdocuments') and not os.environ.get('UPLOAD_MOUNT_DIR'):
|
||||||
|
base_dir = os.path.join(settings.BASE_DIR, 'mnt/s3files/supplierdocuments')
|
||||||
|
file_path = os.path.join(base_dir, s3_key)
|
||||||
|
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||||
|
with open(file_path, 'wb') as f:
|
||||||
|
for chunk in file_obj.chunks():
|
||||||
|
f.write(chunk)
|
||||||
|
|
||||||
|
# Update profile with s3 key
|
||||||
|
profile.aadhar_s3_key = s3_key
|
||||||
|
profile.save()
|
||||||
|
|
||||||
|
# 2. Call Surepass API
|
||||||
|
surepass_token = os.environ.get('SUREPASS_JWT_TOKEN')
|
||||||
|
if not surepass_token:
|
||||||
|
logger.warning("Surepass token not configured. Skipping actual verification.")
|
||||||
|
mock_success_data = {
|
||||||
|
"aadhaar_number": "XXXX XXXX 1234",
|
||||||
|
"name": request.data.get('full_name', "Mock User"),
|
||||||
|
"dob": request.data.get('yob', "1990"),
|
||||||
|
"gender": "M",
|
||||||
|
"address": "123, Mock Address",
|
||||||
|
"face_status": False,
|
||||||
|
"face": "",
|
||||||
|
}
|
||||||
|
kyc, _ = SellerKYC.objects.update_or_create(
|
||||||
|
supplier=user,
|
||||||
|
defaults={
|
||||||
|
'aadhaar_number': mock_success_data['aadhaar_number'],
|
||||||
|
'name': mock_success_data['name'],
|
||||||
|
'dob': mock_success_data['dob'],
|
||||||
|
'gender': mock_success_data['gender'],
|
||||||
|
'address': mock_success_data['address'],
|
||||||
|
'face_base64': mock_success_data['face'],
|
||||||
|
'verification_response': {"mock": True, "data": mock_success_data}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# Send Email
|
||||||
|
send_mail(
|
||||||
|
'Aadhaar Verification Successful',
|
||||||
|
'Your Aadhaar document has been successfully verified.',
|
||||||
|
'noreply@sellercentral.com',
|
||||||
|
[user.email],
|
||||||
|
fail_silently=True,
|
||||||
|
)
|
||||||
|
return Response({'success': True, 'kyc_data': mock_success_data})
|
||||||
|
|
||||||
|
file_obj.seek(0)
|
||||||
|
url = "https://kyc-api.surepass.io/api/v1/aadhaar/upload/eaadhaar"
|
||||||
|
headers = {'Authorization': f'Bearer {surepass_token}'}
|
||||||
|
|
||||||
|
# Build payload
|
||||||
|
payload = {}
|
||||||
|
if request.data.get('yob'):
|
||||||
|
payload['yob'] = request.data.get('yob')
|
||||||
|
if request.data.get('full_name'):
|
||||||
|
payload['full_name'] = request.data.get('full_name')
|
||||||
|
if request.data.get('base64'):
|
||||||
|
payload['base64'] = request.data.get('base64')
|
||||||
|
|
||||||
|
files = [
|
||||||
|
('file', ('eaadhaar.pdf', file_obj, 'application/pdf'))
|
||||||
|
]
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.request("POST", url, headers=headers, data=payload, files=files)
|
||||||
|
resp_json = response.json()
|
||||||
|
|
||||||
|
if response.status_code == 200 and resp_json.get('success'):
|
||||||
|
data = resp_json.get('data', {})
|
||||||
|
kyc, _ = SellerKYC.objects.update_or_create(
|
||||||
|
supplier=user,
|
||||||
|
defaults={
|
||||||
|
'aadhaar_number': data.get('aadhaar_number'),
|
||||||
|
'name': data.get('name'),
|
||||||
|
'dob': data.get('dob'),
|
||||||
|
'gender': data.get('gender'),
|
||||||
|
'address': data.get('address'),
|
||||||
|
'face_base64': data.get('face'),
|
||||||
|
'verification_response': resp_json
|
||||||
|
}
|
||||||
|
)
|
||||||
|
send_mail(
|
||||||
|
'Aadhaar Verification Successful',
|
||||||
|
'Your Aadhaar document has been successfully verified.',
|
||||||
|
'noreply@sellercentral.com',
|
||||||
|
[user.email],
|
||||||
|
fail_silently=True,
|
||||||
|
)
|
||||||
|
return Response({'success': True, 'kyc_data': data})
|
||||||
|
else:
|
||||||
|
return Response({
|
||||||
|
'success': False,
|
||||||
|
'error': resp_json.get('message', 'Verification failed')
|
||||||
|
}, status=status.HTTP_422_UNPROCESSABLE_ENTITY)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error calling Surepass API: {e}")
|
||||||
|
return Response({'error': 'Internal error verifying Aadhaar'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
|
||||||
|
|
@ -340,6 +340,25 @@ def load_s3_secrets():
|
||||||
|
|
||||||
s3_secrets = load_s3_secrets()
|
s3_secrets = load_s3_secrets()
|
||||||
|
|
||||||
|
def load_surepass_secrets():
|
||||||
|
secret_name = "arn:aws:secretsmanager:ap-south-2:764709663363:secret:surpassbeta-r6EcKi"
|
||||||
|
region_name = "ap-south-2"
|
||||||
|
try:
|
||||||
|
session = boto3.session.Session()
|
||||||
|
client = session.client(service_name='secretsmanager', region_name=region_name)
|
||||||
|
get_secret_value_response = client.get_secret_value(SecretId=secret_name)
|
||||||
|
secrets = json.loads(get_secret_value_response['SecretString'])
|
||||||
|
|
||||||
|
if 'apikey' in secrets:
|
||||||
|
os.environ['SUREPASS_JWT_TOKEN'] = str(secrets['apikey'])
|
||||||
|
|
||||||
|
return secrets
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Failed to load Surepass secrets from Secrets Manager: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
surepass_secrets = load_surepass_secrets()
|
||||||
|
|
||||||
AWS_CLOUDFRONT_DOMAIN = os.environ.get('AWS_CLOUDFRONT_DOMAIN', '')
|
AWS_CLOUDFRONT_DOMAIN = os.environ.get('AWS_CLOUDFRONT_DOMAIN', '')
|
||||||
USE_S3 = os.environ.get('USE_S3', 'False').lower() == 'true'
|
USE_S3 = os.environ.get('USE_S3', 'False').lower() == 'true'
|
||||||
AWS_STORAGE_BUCKET_NAME = os.environ.get('AWS_STORAGE_BUCKET_NAME', 'betasupplierdocumentstorage')
|
AWS_STORAGE_BUCKET_NAME = os.environ.get('AWS_STORAGE_BUCKET_NAME', 'betasupplierdocumentstorage')
|
||||||
|
|
|
||||||
|
|
@ -12,3 +12,4 @@ psycopg2-binary
|
||||||
boto3
|
boto3
|
||||||
djangorestframework-simplejwt
|
djangorestframework-simplejwt
|
||||||
|
|
||||||
|
requests
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue