diff --git a/verifier/admin.py b/verifier/admin.py index 40d5050..002493d 100644 --- a/verifier/admin.py +++ b/verifier/admin.py @@ -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") diff --git a/verifier/migrations/0003_pandetails.py b/verifier/migrations/0003_pandetails.py new file mode 100644 index 0000000..078cf3e --- /dev/null +++ b/verifier/migrations/0003_pandetails.py @@ -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'], + }, + ), + ] diff --git a/verifier/models.py b/verifier/models.py index bd275ec..c3719aa 100644 --- a/verifier/models.py +++ b/verifier/models.py @@ -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'})" + diff --git a/verifier/vendors/codeplus.py b/verifier/vendors/codeplus.py index 61513cc..5769df9 100644 --- a/verifier/vendors/codeplus.py +++ b/verifier/vendors/codeplus.py @@ -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"), diff --git a/verifier/views.py b/verifier/views.py index 5c011e0..e2ae2c7 100644 --- a/verifier/views.py +++ b/verifier/views.py @@ -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,