documentverification/verifier/vendors/codeplus.py
2026-10-15 19:42:15 +05:30

311 lines
13 KiB
Python

import logging
import os
from typing import Any, Dict, Optional
import requests
from django.conf import settings
from verifier.engine.base import (
BaseVendor,
DocumentType,
VendorResult,
VerificationStatus,
)
logger = logging.getLogger(__name__)
class CodeplusVendor(BaseVendor):
"""
Codeplus Verification Adapter.
Implements verification workflows for PAN, Aadhaar, Driving License, Voter ID,
Bank Accounts, GSTIN, and custom documents via Codeplus APIs.
"""
name: str = "codeplus"
display_name: str = "Codeplus Verification Services"
priority: int = 10 # Lower number = higher priority in waterfall
is_active: bool = True
supported_documents = [
DocumentType.PAN.value,
DocumentType.AADHAAR.value,
DocumentType.DRIVING_LICENSE.value,
DocumentType.VOTER_ID.value,
DocumentType.PASSPORT.value,
DocumentType.BANK_ACCOUNT.value,
DocumentType.GSTIN.value,
DocumentType.CUSTOM.value,
]
def __init__(self, config: Optional[Dict[str, Any]] = None):
super().__init__(config=config)
self.base_url = (
self.config.get("base_url")
or getattr(settings, "CODEPLUS_BASE_URL", None)
or os.environ.get("CODEPLUS_BASE_URL", "https://api.codeplus.in")
).rstrip("/")
self.api_key = (
self.config.get("api_key")
or getattr(settings, "CODEPLUS_API_KEY", None)
or os.environ.get("CODEPLUS_API_KEY", "")
)
self.client_id = (
self.config.get("client_id")
or getattr(settings, "CODEPLUS_CLIENT_ID", None)
or os.environ.get("CODEPLUS_CLIENT_ID", "")
)
self.client_secret = (
self.config.get("client_secret")
or getattr(settings, "CODEPLUS_CLIENT_SECRET", None)
or os.environ.get("CODEPLUS_CLIENT_SECRET", "")
)
self.timeout = int(self.config.get("timeout", 15))
self.session = requests.Session()
def get_headers(self) -> Dict[str, str]:
"""Construct standard HTTP headers for Codeplus API calls."""
headers = {
"Content-Type": "application/json",
"Accept": "application/json",
}
if self.api_key:
headers["Authorization"] = f"Bearer {self.api_key}"
headers["x-api-key"] = self.api_key
if self.client_id:
headers["x-client-id"] = self.client_id
if self.client_secret:
headers["x-client-secret"] = self.client_secret
return headers
def execute_verification(
self,
document_type: str,
data: Dict[str, Any],
files: Optional[Dict[str, Any]] = None,
) -> VendorResult:
"""
Dispatches verification to specific document handlers.
"""
handlers = {
DocumentType.PAN.value: self.verify_pan,
DocumentType.AADHAAR.value: self.verify_aadhaar,
DocumentType.DRIVING_LICENSE.value: self.verify_driving_license,
DocumentType.VOTER_ID.value: self.verify_voter_id,
DocumentType.PASSPORT.value: self.verify_passport,
DocumentType.BANK_ACCOUNT.value: self.verify_bank_account,
DocumentType.GSTIN.value: self.verify_gstin,
}
handler = handlers.get(document_type, self.verify_generic)
return handler(data, files)
# ──────────────────────────────────────────────────────────────────────────
# Document-Specific Verification Methods
# (Structured for easy payload/endpoint mapping once examples are provided)
# ──────────────────────────────────────────────────────────────────────────
def verify_pan(self, data: Dict[str, Any], files: Optional[Dict[str, Any]] = None) -> VendorResult:
"""
PAN Card Verification.
Expected data inputs:
- pan_number / pan: PAN number (e.g. ABCDE1234F)
- name / full_name: (Optional) Name to compare
- dob: (Optional) Date of Birth (YYYY-MM-DD or DD/MM/YYYY)
"""
endpoint = f"{self.base_url}/v1/pan/verify"
payload = {
"pan": data.get("pan_number") or data.get("pan"),
"name": data.get("name") or data.get("full_name"),
"dob": data.get("dob"),
}
return self._send_request(endpoint, payload=payload, files=files, doc_label="PAN")
def verify_aadhaar(self, data: Dict[str, Any], files: Optional[Dict[str, Any]] = None) -> VendorResult:
"""
Aadhaar Verification (OTP generation, verification, or OCR).
"""
endpoint = f"{self.base_url}/v1/aadhaar/verify"
payload = {
"aadhaar_number": data.get("aadhaar_number") or data.get("aadhaar"),
"otp": data.get("otp"),
"client_ref_id": data.get("client_ref_id"),
}
return self._send_request(endpoint, payload=payload, files=files, doc_label="Aadhaar")
def verify_driving_license(self, data: Dict[str, Any], files: Optional[Dict[str, Any]] = None) -> VendorResult:
"""
Driving License Verification.
"""
endpoint = f"{self.base_url}/v1/dl/verify"
payload = {
"dl_number": data.get("dl_number") or data.get("license_number"),
"dob": data.get("dob"),
}
return self._send_request(endpoint, payload=payload, files=files, doc_label="Driving License")
def verify_voter_id(self, data: Dict[str, Any], files: Optional[Dict[str, Any]] = None) -> VendorResult:
"""
Voter ID (EPIC) Verification.
"""
endpoint = f"{self.base_url}/v1/voter-id/verify"
payload = {
"epic_number": data.get("epic_number") or data.get("voter_id"),
}
return self._send_request(endpoint, payload=payload, files=files, doc_label="Voter ID")
def verify_passport(self, data: Dict[str, Any], files: Optional[Dict[str, Any]] = None) -> VendorResult:
"""
Passport Verification.
"""
endpoint = f"{self.base_url}/v1/passport/verify"
payload = {
"passport_number": data.get("passport_number"),
"dob": data.get("dob"),
"file_number": data.get("file_number"),
}
return self._send_request(endpoint, payload=payload, files=files, doc_label="Passport")
def verify_bank_account(self, data: Dict[str, Any], files: Optional[Dict[str, Any]] = None) -> VendorResult:
"""
Bank Account / Penny Drop Verification.
"""
endpoint = f"{self.base_url}/v1/bank/verify"
payload = {
"account_number": data.get("account_number") or data.get("bank_account"),
"ifsc": data.get("ifsc"),
"name": data.get("name"),
}
return self._send_request(endpoint, payload=payload, files=files, doc_label="Bank Account")
def verify_gstin(self, data: Dict[str, Any], files: Optional[Dict[str, Any]] = None) -> VendorResult:
"""
GSTIN Business Verification.
"""
endpoint = f"{self.base_url}/v1/gstin/verify"
payload = {
"gstin": data.get("gstin"),
}
return self._send_request(endpoint, payload=payload, files=files, doc_label="GSTIN")
def verify_generic(self, data: Dict[str, Any], files: Optional[Dict[str, Any]] = None) -> VendorResult:
"""Generic document verification fallback."""
endpoint = f"{self.base_url}/v1/document/verify"
return self._send_request(endpoint, payload=data, files=files, doc_label="Generic Document")
# ──────────────────────────────────────────────────────────────────────────
# Internal HTTP Dispatcher & Response Normalizer
# ──────────────────────────────────────────────────────────────────────────
def _send_request(
self,
endpoint: str,
payload: Dict[str, Any],
files: Optional[Dict[str, Any]] = None,
doc_label: str = "Document",
) -> VendorResult:
"""
Executes HTTP call to Codeplus and maps response into standardized VendorResult.
Catches errors to trigger fallback waterfall if Codeplus fails.
"""
headers = self.get_headers()
# Clean payload of None values
clean_payload = {k: v for k, v in payload.items() if v is not None}
try:
if files:
headers.pop("Content-Type", None)
response = self.session.post(
endpoint,
data=clean_payload,
files=files,
headers=headers,
timeout=self.timeout,
)
else:
response = self.session.post(
endpoint,
json=clean_payload,
headers=headers,
timeout=self.timeout,
)
try:
raw_json = response.json()
except Exception:
raw_json = {"text": response.text}
# Check HTTP status
if response.status_code >= 400:
error_msg = (
raw_json.get("message")
or raw_json.get("error")
or f"Codeplus HTTP {response.status_code} Error"
)
logger.warning(
"Codeplus %s verification failed with HTTP %d: %s",
doc_label,
response.status_code,
error_msg,
)
return VendorResult(
vendor_name=self.name,
is_success=False,
status=VerificationStatus.FAILED if response.status_code < 500 else VerificationStatus.ERROR,
raw_response=raw_json,
error_code=f"HTTP_{response.status_code}",
error_message=str(error_msg),
retryable=response.status_code >= 500 or response.status_code in (429, 408),
)
# Codeplus success validation (can be tuned once exact example response is shared)
status_field = str(raw_json.get("status", "")).upper()
is_successful = status_field in ("SUCCESS", "VALID", "COMPLETED", "TRUE", "1") or raw_json.get("valid") is True
if is_successful or ("data" in raw_json and "error" not in raw_json):
extracted_data = raw_json.get("data") if isinstance(raw_json.get("data"), dict) else raw_json
return VendorResult(
vendor_name=self.name,
is_success=True,
status=VerificationStatus.SUCCESS,
data=extracted_data,
raw_response=raw_json,
)
else:
msg = raw_json.get("message") or raw_json.get("error") or "Document verification returned invalid status"
return VendorResult(
vendor_name=self.name,
is_success=False,
status=VerificationStatus.FAILED,
data=raw_json.get("data", {}),
raw_response=raw_json,
error_code="INVALID_STATUS",
error_message=str(msg),
retryable=False,
)
except requests.exceptions.Timeout as exc:
logger.warning("Codeplus %s call timed out: %s", doc_label, exc)
return VendorResult(
vendor_name=self.name,
is_success=False,
status=VerificationStatus.ERROR,
error_code="TIMEOUT",
error_message=f"Request to Codeplus timed out: {exc}",
retryable=True,
)
except requests.exceptions.RequestException as exc:
logger.warning("Codeplus %s network exception: %s", doc_label, exc)
return VendorResult(
vendor_name=self.name,
is_success=False,
status=VerificationStatus.ERROR,
error_code="NETWORK_ERROR",
error_message=f"Network error connecting to Codeplus: {exc}",
retryable=True,
)