feat: add Bank Verification Full Details (v4) and fetch bank_name and branch
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 36s

This commit is contained in:
vickytechkey 2026-10-17 15:46:32 +05:30
parent 71058e84c9
commit 6521343ea1
3 changed files with 95 additions and 46 deletions

View file

@ -18,6 +18,8 @@ class DocumentType(str, Enum):
BANK_VERIFY_PD = "bank_verify_pd"
BANK_VERIFY_V5 = "bank_verify_v5"
BANK_PD = "bank_pd"
BANK_VERIFY_FULL = "bank_verify_full"
BANK_VERIFY_V4 = "bank_verify_v4"
GSTIN = "gstin"
GST = "gst"
GST_VERIFY = "gst_verify"

View file

@ -3,6 +3,7 @@ import os
from typing import Any, Dict, Optional
import requests
from django.conf import settings
from django.utils import timezone
from verifier.engine.base import (
BaseVendor,
@ -36,6 +37,8 @@ class CodeplusVendor(BaseVendor):
DocumentType.VOTER_ID.value,
DocumentType.PASSPORT.value,
DocumentType.BANK_ACCOUNT.value,
DocumentType.BANK_VERIFY_FULL.value,
DocumentType.BANK_VERIFY_V4.value,
DocumentType.BANK_VERIFY_PD.value,
DocumentType.BANK_VERIFY_V5.value,
DocumentType.BANK_PD.value,
@ -116,6 +119,8 @@ class CodeplusVendor(BaseVendor):
DocumentType.VOTER_ID.value: self.verify_voter_id,
DocumentType.PASSPORT.value: self.verify_passport,
DocumentType.BANK_ACCOUNT.value: self.verify_bank_account,
DocumentType.BANK_VERIFY_FULL.value: self.verify_bank_account_v4,
DocumentType.BANK_VERIFY_V4.value: self.verify_bank_account_v4,
DocumentType.BANK_VERIFY_PD.value: self.verify_bank_verify_pd_v5,
DocumentType.BANK_VERIFY_V5.value: self.verify_bank_verify_pd_v5,
DocumentType.BANK_PD.value: self.verify_bank_verify_pd_v5,
@ -579,30 +584,29 @@ class CodeplusVendor(BaseVendor):
def verify_bank_account(self, data: Dict[str, Any], files: Optional[Dict[str, Any]] = None) -> VendorResult:
"""
Bank Account Verification Dispatcher:
- Defaults to Bank Verify (Penny Drop - v5) Recommended API (`verify_bank_verify_pd_v5`)
- If mode or version is 'v4', 'detailed', or 'full', routes to `verify_bank_account_v4`
- If v5 encounters an upstream retryable failure (5xx or timeout), falls back to v4
- Prioritizes Bank Verification (Full Details - v4) (`verify_bank_account_v4`)
which retrieves beneficiary name, bank_name, branch, and city via penny drop routing
- If mode or version is 'v5', routes to `verify_bank_verify_pd_v5`
- Seamless failover across v4 and v5
"""
mode = str(data.get("mode", "")).lower()
version = str(data.get("version", "")).lower()
if version == "v4" or mode in ("v4", "detailed", "full"):
return self.verify_bank_account_v4(data, files)
if version in ("v5", "pd") or mode in ("v5", "penny_drop", "pd"):
return self.verify_bank_verify_pd_v5(data, files)
# Primary execution: Bank Verify (Penny Drop - v5)
v5_result = self.verify_bank_verify_pd_v5(data, files)
if v5_result.is_success:
return v5_result
# Primary execution: Bank Verification (Full Details - v4) to fetch bank_name and branch
v4_result = self.verify_bank_account_v4(data, files)
if v4_result.is_success:
return v4_result
# Fallback locally to v4 if upstream server error / timeout or if upstream returns v4 response structure
is_v4_payload = isinstance(v5_result.raw_response, dict) and "bank_data" in v5_result.raw_response.get("data", {})
if v5_result.retryable or is_v4_payload:
logger.info("Codeplus Bank Verify fallback to v4 triggered (retryable=%s, is_v4=%s)...", v5_result.retryable, is_v4_payload)
v4_result = self.verify_bank_account_v4(data, files)
if v4_result.is_success:
return v4_result
# Fallback to v5 Penny Drop if v4 failed with retryable/network error
if v4_result.retryable:
v5_result = self.verify_bank_verify_pd_v5(data, files)
if v5_result.is_success:
return v5_result
return v5_result
return v4_result
def verify_bank_verify_pd_v5(self, data: Dict[str, Any], files: Optional[Dict[str, Any]] = None) -> VendorResult:
"""
@ -941,35 +945,55 @@ class CodeplusVendor(BaseVendor):
error_block = raw_json.get("error") if isinstance(raw_json.get("error"), dict) else {}
code = str(data_block.get("code") or "")
bank_data = data_block.get("bank_account_data") or data_block.get("bank_data") or data_block or raw_json
beneficiary_name = (
bank_data.get("name")
or bank_data.get("name_at_bank")
or bank_data.get("beneficiary_name")
or bank_data.get("account_holder_name")
or data_block.get("name")
)
bank_name = bank_data.get("bank_name") or bank_data.get("bank")
branch = bank_data.get("branch") or bank_data.get("bank_branch")
city = bank_data.get("city")
utr = bank_data.get("utr")
account_status = bank_data.get("account_status")
# Success condition: HTTP 200 and either code 1000 or valid bank verification payload
is_code_success = code in ("1000", "SUCCESS", "200")
is_body_success = response.status_code == 200 and not error_block and ("data" in raw_json or "account_exists" in raw_json)
if is_code_success or is_body_success:
bank_data = data_block.get("bank_data") or data_block or raw_json
beneficiary_name = (
bank_data.get("name_at_bank")
or bank_data.get("beneficiary_name")
or bank_data.get("account_holder_name")
or bank_data.get("name")
or data_block.get("name")
)
is_code_success = code in ("1000", "SUCCESS", "200") or bool(beneficiary_name and response.status_code == 200 and not error_block)
if is_code_success and not error_block:
masked_acc = f"********{account_number[-4:]}" if len(account_number) >= 4 else account_number
normalized_result = {
"document_type": "BANK_ACCOUNT",
"verification_mode": "full_details_v4",
"account_number": account_number,
"masked_account_number": masked_acc,
"ifsc": ifsc,
"account_ifsc": ifsc,
"beneficiary_name": beneficiary_name,
"micr": bank_data.get("micr") or bank_data.get("micr_code"),
"bank_name": bank_data.get("bank_name") or bank_data.get("bank"),
"branch": bank_data.get("branch"),
"city": bank_data.get("city"),
"account_holder_name": beneficiary_name,
"bank_name": bank_name,
"branch": branch,
"bank_branch": branch,
"city": city,
"state": bank_data.get("state"),
"pincode": bank_data.get("pincode") or bank_data.get("pin_code"),
"account_exists": bank_data.get("account_exists", True),
"micr": bank_data.get("micr") or bank_data.get("micr_code"),
"utr": utr,
"account_status": account_status,
"npci_response_code": bank_data.get("npci_response_code"),
"npci_description": bank_data.get("npci_description"),
"is_valid": True,
"account_exists": True,
"failure_reason": None,
"npci_error_code": None,
"user_consent": str(data.get("user_consent") or "true"),
"bank_details": bank_data,
"request_id": raw_json.get("request_id"),
"transaction_id": raw_json.get("transaction_id"),
"tranx_id": raw_json.get("transaction_id") or raw_json.get("request_id"),
"timestamp": str(raw_json.get("timestamp") or timezone.now().isoformat()),
}
return VendorResult(
@ -980,20 +1004,43 @@ class CodeplusVendor(BaseVendor):
raw_response=raw_json,
)
# Failure / Error handling
# Failure / Error handling (e.g. code 1001 "Provided invalid Account Number." or 400 "Invalid IFSC Code")
is_server_error = response.status_code >= 500
if error_block:
err_code = str(error_block.get("code") or f"HTTP_{response.status_code}")
err_message = error_block.get("message") or "Bank verification failed"
elif data_block:
err_code = str(data_block.get("code") or f"HTTP_{response.status_code}")
err_message = data_block.get("message") or "Bank verification failed"
else:
err_code = f"HTTP_{response.status_code}"
err_code = str(code or (error_block.get("code") if error_block else f"HTTP_{response.status_code}"))
err_message = data_block.get("message") or ""
if not err_message and error_block:
fields = error_block.get("metadata", {}).get("fields", [])
if fields and isinstance(fields, list):
field_msgs = [f.get("message") for f in fields if f.get("message")]
if field_msgs:
err_message = "; ".join(field_msgs)
if not err_message:
err_message = error_block.get("message") or "Bank verification failed"
if not err_message:
err_message = raw_json.get("message") or "Bank verification failed"
normalized_result = {
"document_type": "BANK_ACCOUNT",
"verification_mode": "full_details_v4",
"account_number": account_number,
"ifsc": ifsc,
"account_ifsc": ifsc,
"beneficiary_name": None,
"account_holder_name": None,
"bank_name": None,
"branch": None,
"bank_branch": None,
"is_valid": False,
"account_exists": False,
"failure_reason": err_message,
"npci_error_code": err_code,
"bank_details": data_block,
"request_id": raw_json.get("request_id"),
"transaction_id": raw_json.get("transaction_id"),
}
logger.warning(
"Codeplus Bank verification failed: status_code=%d, code=%s, message=%s",
"Codeplus Bank verification (v4 Full Details) failed: status_code=%d, code=%s, message=%s",
response.status_code,
err_code,
err_message,
@ -1003,7 +1050,7 @@ class CodeplusVendor(BaseVendor):
vendor_name=self.name,
is_success=False,
status=VerificationStatus.ERROR if is_server_error else VerificationStatus.FAILED,
data=data_block,
data=normalized_result,
raw_response=raw_json,
error_code=err_code,
error_message=err_message,

View file

@ -181,7 +181,7 @@ class DocumentVerifyView(APIView):
# If bank verification was performed and seller_id is supplied, persist directly to seller_bank_verification table
seller_id = input_data.get("seller_id") or request.data.get("seller_id")
if doc_type in ["bank_account", "bank_verify_pd", "bank_verify_v5", "bank_pd"] and seller_id:
if doc_type in ["bank_account", "bank_verify_pd", "bank_verify_v5", "bank_pd", "bank_verify_full", "bank_verify_v4"] and seller_id:
try:
b_data = engine_result.data or {}
last_err_code = engine_result.attempts[-1].get("error_code") if engine_result.attempts else None