feat: add core verification views, models, admin config, and CodePlus vendor integration
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 34s

This commit is contained in:
vickytechkey 2026-10-16 14:35:22 +05:30
parent 51e74e2043
commit 4e1ea0fcf2
5 changed files with 214 additions and 8 deletions

View file

@ -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")

View 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'],
},
),
]

View file

@ -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'})"

View file

@ -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"),

View file

@ -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,