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