From 6521343ea1504c007a092b019a2ed4244ea5c08e Mon Sep 17 00:00:00 2001 From: vickytechkey Date: Sat, 17 Oct 2026 15:46:32 +0530 Subject: [PATCH] feat: add Bank Verification Full Details (v4) and fetch bank_name and branch --- verifier/engine/base.py | 2 + verifier/vendors/codeplus.py | 137 +++++++++++++++++++++++------------ verifier/views.py | 2 +- 3 files changed, 95 insertions(+), 46 deletions(-) diff --git a/verifier/engine/base.py b/verifier/engine/base.py index 42bf220..df01edf 100644 --- a/verifier/engine/base.py +++ b/verifier/engine/base.py @@ -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" diff --git a/verifier/vendors/codeplus.py b/verifier/vendors/codeplus.py index e903e88..fdc1a6d 100644 --- a/verifier/vendors/codeplus.py +++ b/verifier/vendors/codeplus.py @@ -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, diff --git a/verifier/views.py b/verifier/views.py index 12480cb..eb6e356 100644 --- a/verifier/views.py +++ b/verifier/views.py @@ -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