All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 31s
311 lines
13 KiB
Python
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,
|
|
)
|