feat: add core verification views, models, admin config, and CodePlus vendor integration
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 34s
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 34s
This commit is contained in:
parent
51e74e2043
commit
4e1ea0fcf2
5 changed files with 214 additions and 8 deletions
|
|
@ -1,5 +1,5 @@
|
|||
from django.contrib import admin
|
||||
from .models import VendorConfig, VerificationTransaction, VendorAttemptLog
|
||||
from .models import VendorConfig, VerificationTransaction, VendorAttemptLog, PANDetails
|
||||
|
||||
|
||||
class VendorAttemptLogInline(admin.TabularInline):
|
||||
|
|
@ -53,3 +53,22 @@ class VerificationTransactionAdmin(admin.ModelAdmin):
|
|||
"updated_at",
|
||||
)
|
||||
inlines = [VendorAttemptLogInline]
|
||||
|
||||
|
||||
@admin.register(PANDetails)
|
||||
class PANDetailsAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
"document_id",
|
||||
"name",
|
||||
"category",
|
||||
"date_of_birth",
|
||||
"gender",
|
||||
"city",
|
||||
"state",
|
||||
"pincode",
|
||||
"aadhaar_linked",
|
||||
"created_at",
|
||||
)
|
||||
list_filter = ("category", "gender", "aadhaar_linked", "taxpayer_profile", "state", "created_at")
|
||||
search_fields = ("document_id", "name", "email", "phone", "city", "pincode", "request_id")
|
||||
readonly_fields = ("created_at", "updated_at")
|
||||
|
|
|
|||
51
verifier/migrations/0003_pandetails.py
Normal file
51
verifier/migrations/0003_pandetails.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# Generated by Django 6.1 on 2026-10-16 09:05
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('verifier', '0002_verificationtransaction_response_payload'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PANDetails',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('document_type', models.CharField(db_index=True, default='PAN', max_length=20)),
|
||||
('document_id', models.CharField(db_index=True, help_text='10-digit PAN Number (e.g. AVEPV5707G)', max_length=20)),
|
||||
('name', models.CharField(db_index=True, help_text='Full Name on PAN card', max_length=255)),
|
||||
('first_name', models.CharField(blank=True, max_length=150, null=True)),
|
||||
('last_name', models.CharField(blank=True, max_length=150, null=True)),
|
||||
('category', models.CharField(blank=True, help_text='PAN holder category (e.g. P for Person/Individual)', max_length=10, null=True)),
|
||||
('date_of_birth', models.CharField(blank=True, help_text='YYYY-MM-DD or DD/MM/YYYY', max_length=50, null=True)),
|
||||
('gender', models.CharField(blank=True, max_length=20, null=True)),
|
||||
('masked_aadhaar_number', models.CharField(blank=True, help_text='e.g. XXXXXXXX5139', max_length=30, null=True)),
|
||||
('aadhaar_linked', models.BooleanField(default=False, help_text='Whether Aadhaar is seeded/linked with PAN')),
|
||||
('taxpayer_profile', models.BooleanField(default=False, help_text='Active taxpayer profile flag')),
|
||||
('email', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('phone', models.CharField(blank=True, max_length=50, null=True)),
|
||||
('address_line_1', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('street', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('city', models.CharField(blank=True, db_index=True, max_length=150, null=True)),
|
||||
('state', models.CharField(blank=True, db_index=True, max_length=100, null=True)),
|
||||
('pincode', models.CharField(blank=True, db_index=True, max_length=20, null=True)),
|
||||
('address_data', models.JSONField(blank=True, default=dict, help_text='Full structured address object', null=True)),
|
||||
('request_id', models.CharField(blank=True, db_index=True, max_length=100, null=True)),
|
||||
('vendor_txn_id', models.CharField(blank=True, db_index=True, max_length=100, null=True)),
|
||||
('full_response', models.JSONField(blank=True, default=dict, help_text='Entire uncompressed response payload from vendor', null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('transaction', models.ForeignKey(blank=True, help_text='Associated verification transaction', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='pan_details_record', to='verifier.verificationtransaction')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'PAN Detail',
|
||||
'verbose_name_plural': 'PAN Details',
|
||||
'db_table': 'pan_details',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -93,3 +93,59 @@ class VendorAttemptLog(models.Model):
|
|||
|
||||
def __str__(self):
|
||||
return f"Tx: {self.transaction.reference_id} | #{self.attempt_number} {self.vendor_name} -> {self.status}"
|
||||
|
||||
|
||||
class PANDetails(models.Model):
|
||||
"""
|
||||
Dedicated table storing granular fields from Detailed PAN verification
|
||||
for analysis, reporting, and KYC intelligence.
|
||||
"""
|
||||
transaction = models.ForeignKey(
|
||||
VerificationTransaction,
|
||||
related_name="pan_details_record",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Associated verification transaction",
|
||||
)
|
||||
document_type = models.CharField(max_length=20, default="PAN", db_index=True)
|
||||
document_id = models.CharField(max_length=20, db_index=True, help_text="10-digit PAN Number (e.g. AVEPV5707G)")
|
||||
name = models.CharField(max_length=255, db_index=True, help_text="Full Name on PAN card")
|
||||
first_name = models.CharField(max_length=150, blank=True, null=True)
|
||||
last_name = models.CharField(max_length=150, blank=True, null=True)
|
||||
category = models.CharField(max_length=10, blank=True, null=True, help_text="PAN holder category (e.g. P for Person/Individual)")
|
||||
date_of_birth = models.CharField(max_length=50, blank=True, null=True, help_text="YYYY-MM-DD or DD/MM/YYYY")
|
||||
gender = models.CharField(max_length=20, blank=True, null=True)
|
||||
masked_aadhaar_number = models.CharField(max_length=30, blank=True, null=True, help_text="e.g. XXXXXXXX5139")
|
||||
aadhaar_linked = models.BooleanField(default=False, help_text="Whether Aadhaar is seeded/linked with PAN")
|
||||
taxpayer_profile = models.BooleanField(default=False, help_text="Active taxpayer profile flag")
|
||||
|
||||
# Contact Info
|
||||
email = models.CharField(max_length=255, blank=True, null=True)
|
||||
phone = models.CharField(max_length=50, blank=True, null=True)
|
||||
|
||||
# Address breakdown columns for regional & geographical analytics
|
||||
address_line_1 = models.CharField(max_length=255, blank=True, null=True)
|
||||
street = models.CharField(max_length=255, blank=True, null=True)
|
||||
city = models.CharField(max_length=150, blank=True, null=True, db_index=True)
|
||||
state = models.CharField(max_length=100, blank=True, null=True, db_index=True)
|
||||
pincode = models.CharField(max_length=20, blank=True, null=True, db_index=True)
|
||||
address_data = models.JSONField(default=dict, blank=True, null=True, help_text="Full structured address object")
|
||||
|
||||
# API tracking & Complete JSON payloads for future data science & audit
|
||||
request_id = models.CharField(max_length=100, blank=True, null=True, db_index=True)
|
||||
vendor_txn_id = models.CharField(max_length=100, blank=True, null=True, db_index=True)
|
||||
full_response = models.JSONField(default=dict, blank=True, null=True, help_text="Entire uncompressed response payload from vendor")
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "pan_details"
|
||||
ordering = ["-created_at"]
|
||||
verbose_name = "PAN Detail"
|
||||
verbose_name_plural = "PAN Details"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.document_id} - {self.name} ({self.city or 'Unknown'}, {self.state or 'Unknown'})"
|
||||
|
||||
|
|
|
|||
6
verifier/vendors/codeplus.py
vendored
6
verifier/vendors/codeplus.py
vendored
|
|
@ -467,8 +467,10 @@ class CodeplusVendor(BaseVendor):
|
|||
error_block = raw_json.get("error") if isinstance(raw_json.get("error"), dict) else {}
|
||||
code = str(data_block.get("code") or "")
|
||||
|
||||
# Success condition: HTTP 200 and Code 1000 ("Valid PAN.")
|
||||
if response.status_code == 200 and code == "1000":
|
||||
# Success condition: HTTP 200 and Code 1000 ("Valid PAN.") or valid data block
|
||||
is_code_success = code in ("1000", "SUCCESS", "200")
|
||||
is_body_success = response.status_code == 200 and not error_block and ("data" in raw_json and bool(data_block))
|
||||
if is_code_success or is_body_success:
|
||||
pan_data = data_block.get("pan_data") or data_block
|
||||
normalized_result = {
|
||||
"document_type": pan_data.get("document_type", "PAN"),
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from rest_framework.views import APIView
|
|||
# Import engine and vendors to ensure default registration
|
||||
import verifier.vendors # noqa: F401
|
||||
from verifier.engine import engine, registry
|
||||
from .models import VerificationTransaction, VendorAttemptLog
|
||||
from .models import VerificationTransaction, VendorAttemptLog, PANDetails
|
||||
from .serializers import (
|
||||
VerifyDocumentRequestSerializer,
|
||||
VerifySellerPanGstRequestSerializer,
|
||||
|
|
@ -146,6 +146,39 @@ class DocumentVerifyView(APIView):
|
|||
transaction.response_payload = response_payload
|
||||
transaction.save(update_fields=["response_payload"])
|
||||
|
||||
# If pan or pan_detailed was verified, persist directly to pan_details table
|
||||
if doc_type in ["pan", "pan_detailed"] and engine_result.is_success and engine_result.data:
|
||||
try:
|
||||
p_data = engine_result.data.get("pan_details") or engine_result.data
|
||||
addr = p_data.get("address_data") or {}
|
||||
PANDetails.objects.create(
|
||||
transaction=transaction,
|
||||
document_type=str(p_data.get("document_type") or "PAN"),
|
||||
document_id=str(p_data.get("document_id") or p_data.get("pan_number") or ""),
|
||||
name=str(p_data.get("name") or p_data.get("full_name") or ""),
|
||||
first_name=p_data.get("first_name"),
|
||||
last_name=p_data.get("last_name"),
|
||||
category=p_data.get("category"),
|
||||
date_of_birth=str(p_data.get("date_of_birth") or p_data.get("dob") or ""),
|
||||
gender=p_data.get("gender"),
|
||||
masked_aadhaar_number=p_data.get("masked_aadhaar_number"),
|
||||
aadhaar_linked=bool(p_data.get("aadhaar_linked") or p_data.get("aadhaar_seeding_status") == "LINKED"),
|
||||
taxpayer_profile=bool(p_data.get("taxpayer_profile")),
|
||||
email=p_data.get("email"),
|
||||
phone=p_data.get("phone"),
|
||||
address_line_1=addr.get("line_1"),
|
||||
street=addr.get("street"),
|
||||
city=addr.get("city"),
|
||||
state=addr.get("state"),
|
||||
pincode=addr.get("pincode"),
|
||||
address_data=addr,
|
||||
request_id=p_data.get("request_id") or engine_result.data.get("request_id"),
|
||||
vendor_txn_id=p_data.get("transaction_id") or engine_result.data.get("transaction_id"),
|
||||
full_response=engine_result.data,
|
||||
)
|
||||
except Exception as pan_err:
|
||||
logger.error("Failed to store PAN record in pan_details table: %s", pan_err)
|
||||
|
||||
http_status = status.HTTP_200_OK if engine_result.is_success else status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
return Response(response_payload, status=http_status)
|
||||
|
||||
|
|
@ -175,15 +208,29 @@ class SellerPanGstVerifyView(APIView):
|
|||
|
||||
start_time = time.time()
|
||||
|
||||
# Step 1: Verify PAN via waterfall engine
|
||||
# Step 1: Verify PAN via waterfall engine using pan_detailed API
|
||||
pan_result = engine.verify(
|
||||
document_type="pan",
|
||||
data={"pan": pan},
|
||||
document_type="pan_detailed",
|
||||
data={"pan_number": pan, "pan": pan},
|
||||
preferred_vendor=preferred_vendor,
|
||||
)
|
||||
|
||||
all_attempts = list(pan_result.attempts)
|
||||
|
||||
# Fallback to standard PAN if detailed PAN fails or is not enabled for vendor
|
||||
if not pan_result.is_success:
|
||||
logger.info("Detailed PAN lookup not successful, attempting standard PAN verification...")
|
||||
pan_basic_result = engine.verify(
|
||||
document_type="pan",
|
||||
data={"pan": pan, "pan_number": pan},
|
||||
preferred_vendor=preferred_vendor,
|
||||
)
|
||||
for att in pan_basic_result.attempts:
|
||||
att["attempt_number"] = len(all_attempts) + 1
|
||||
all_attempts.append(att)
|
||||
if pan_basic_result.is_success:
|
||||
pan_result = pan_basic_result
|
||||
|
||||
if not pan_result.is_success:
|
||||
total_latency = (time.time() - start_time) * 1000
|
||||
tx = VerificationTransaction.objects.create(
|
||||
|
|
@ -223,7 +270,7 @@ class SellerPanGstVerifyView(APIView):
|
|||
tx.save(update_fields=["response_payload"])
|
||||
return Response(res_payload, status=status.HTTP_422_UNPROCESSABLE_ENTITY)
|
||||
|
||||
# PAN is valid, extract verified name
|
||||
# PAN is valid, extract verified name and detailed attributes
|
||||
pan_data = pan_result.data or {}
|
||||
verified_pan_name = (
|
||||
pan_data.get("name")
|
||||
|
|
@ -392,6 +439,37 @@ class SellerPanGstVerifyView(APIView):
|
|||
latency_ms=att.get("latency_ms", 0.0),
|
||||
)
|
||||
|
||||
# Step 5: Store full PAN detailed response in dedicated 'pan_details' table for future analysis
|
||||
try:
|
||||
addr = pan_data.get("address_data") or {}
|
||||
PANDetails.objects.create(
|
||||
transaction=tx,
|
||||
document_type=str(pan_data.get("document_type") or "PAN"),
|
||||
document_id=str(pan_data.get("document_id") or pan),
|
||||
name=str(pan_data.get("name") or verified_pan_name or seller_name),
|
||||
first_name=pan_data.get("first_name"),
|
||||
last_name=pan_data.get("last_name"),
|
||||
category=pan_data.get("category"),
|
||||
date_of_birth=str(pan_data.get("date_of_birth") or pan_data.get("dob") or ""),
|
||||
gender=pan_data.get("gender"),
|
||||
masked_aadhaar_number=pan_data.get("masked_aadhaar_number"),
|
||||
aadhaar_linked=bool(pan_data.get("aadhaar_linked") or pan_data.get("aadhaar_seeding_status") == "LINKED"),
|
||||
taxpayer_profile=bool(pan_data.get("taxpayer_profile")),
|
||||
email=pan_data.get("email"),
|
||||
phone=pan_data.get("phone"),
|
||||
address_line_1=addr.get("line_1"),
|
||||
street=addr.get("street"),
|
||||
city=addr.get("city"),
|
||||
state=addr.get("state"),
|
||||
pincode=addr.get("pincode"),
|
||||
address_data=addr,
|
||||
request_id=pan_data.get("request_id"),
|
||||
vendor_txn_id=pan_data.get("transaction_id"),
|
||||
full_response=pan_data,
|
||||
)
|
||||
except Exception as db_err:
|
||||
logger.error("Failed to store record in pan_details table: %s", db_err)
|
||||
|
||||
res_payload = {
|
||||
"reference_id": str(tx.reference_id),
|
||||
"is_success": overall_success,
|
||||
|
|
|
|||
Loading…
Reference in a new issue