neft_inward

This commit is contained in:
2026-03-07 19:36:00 +05:30
commit 2073b3fef5
17 changed files with 1786 additions and 0 deletions

6
db/__init__.py Normal file
View File

@@ -0,0 +1,6 @@
"""Database module for ACH file processing."""
from .oracle_connector import OracleConnector
from .repository import Repository
__all__ = ['OracleConnector', 'Repository']

81
db/models.py Normal file
View File

@@ -0,0 +1,81 @@
#!/usr/bin/env python3
"""
Data models for ACH file processing.
Represents database records and transactions.
"""
from dataclasses import dataclass, asdict
from datetime import date, datetime
from decimal import Decimal
from typing import Optional
@dataclass
class NEFTInwardRecord:
"""Represents a parsed NEFT Inward transaction mapped to DB columns."""
txnind: str # VARCHAR2(2), default "CR"
jrnl_id: str # VARCHAR2(4000), NOT NULL
ref_no: str # VARCHAR2(400), NOT NULL
txn_date: str # VARCHAR2(100), NOT NULL
txn_amt: Optional[Decimal] # NUMBER(17,2)
sender_ifsc: str # VARCHAR2(400)
reciever_ifsc: str # VARCHAR2(400)
sender_acct_no: str # VARCHAR2(400)
sender_acct_name: str # VARCHAR2(400)
remitter_detail: str # VARCHAR2(400)
remitter_info: str # VARCHAR2(400)
recvr_acct_no: str # VARCHAR2(400)
recvr_acct_name: str # VARCHAR2(400)
status: str # VARCHAR2(400)
reject_code: str # VARCHAR2(400)
reject_reason: str # VARCHAR2(400)
benef_address: str # VARCHAR2(400)
msg_type: str # VARCHAR2(400)
def to_dict(self):
"""Convert to dictionary for DB insertion."""
return {
"TXNIND": self.txnind,
"JRNL_ID": self.jrnl_id,
"REF_NO": self.ref_no,
"TXN_DATE": self.txn_date,
"TXN_AMT": self.txn_amt,
"SENDER_IFSC": self.sender_ifsc,
"RECIEVER_IFSC": self.reciever_ifsc,
"SENDER_ACCT_NO": self.sender_acct_no,
"SENDER_ACCT_NAME": self.sender_acct_name,
"REMITTER_DETAIL": self.remitter_detail,
"REMITTER_INFO": self.remitter_info,
"RECVR_ACCT_NO": self.recvr_acct_no,
"RECVR_ACCT_NAME": self.recvr_acct_name,
"STATUS": self.status,
"REJECT_CODE": self.reject_code,
"REJECT_REASON": self.reject_reason,
"BENEF_ADDRESS": self.benef_address,
"MSG_TYPE": self.msg_type,
}
@dataclass
class ProcessedFile:
"""Represents a processed file record for ach_processed_files table."""
filename: str
bankcode: str
file_path: str
transaction_count: int
status: str = 'SUCCESS'
error_message: Optional[str] = None
processed_at: Optional[datetime] = None
def to_dict(self):
"""Convert to dictionary for database insertion."""
return {
'filename': self.filename,
'bankcode': self.bankcode,
'file_path': self.file_path,
'transaction_count': self.transaction_count,
'status': self.status,
'error_message': self.error_message,
'processed_at': self.processed_at or datetime.now(),
}

111
db/oracle_connector.py Normal file
View File

@@ -0,0 +1,111 @@
#!/usr/bin/env python3
"""
Oracle database connection pool manager using oracledb.
Manages connections with pooling and health checks.
oracledb is the modern, simpler replacement for cx_Oracle.
No Oracle Instant Client required - uses Thick or Thin mode.
"""
import oracledb
from logging_config import get_logger
from config import get_config
logger = get_logger(__name__)
class OracleConnector:
"""Manages Oracle database connections with pooling."""
def __init__(self):
"""Initialize connection pool."""
self.pool = None
self.config = get_config()
def initialize_pool(self):
"""Create connection pool."""
try:
# Build connection string for oracledb
# Format: user/password@host:port/service_name
connection_string = (
f"{self.config.db_user}/{self.config.db_password}@"
f"{self.config.db_host}:{self.config.db_port}/{self.config.db_service_name}"
)
# Create connection pool using oracledb API
# Note: oracledb uses 'min' and 'max' for pool sizing
self.pool = oracledb.create_pool(
dsn=connection_string,
min=self.config.db_pool_min,
max=self.config.db_pool_max,
increment=1,
)
logger.debug(f"Oracle connection pool initialized: min={self.config.db_pool_min}, max={self.config.db_pool_max}")
return True
except oracledb.DatabaseError as e:
logger.error(f"Failed to initialize connection pool: {e}", exc_info=True)
return False
except Exception as e:
logger.error(f"Unexpected error initializing pool: {e}", exc_info=True)
return False
def get_connection(self):
"""Get connection from pool."""
if not self.pool:
self.initialize_pool()
try:
conn = self.pool.acquire()
logger.debug("Connection acquired from pool")
return conn
except oracledb.DatabaseError as e:
logger.error(f"Failed to acquire connection: {e}", exc_info=True)
raise
except Exception as e:
logger.error(f"Unexpected error acquiring connection: {e}", exc_info=True)
raise
def close_pool(self):
"""Close connection pool."""
if self.pool:
try:
self.pool.close()
logger.info("Connection pool closed")
except Exception as e:
logger.error(f"Error closing pool: {e}")
def test_connection(self):
"""Test database connectivity."""
try:
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute("SELECT 1 FROM dual")
result = cursor.fetchone()
cursor.close()
conn.close()
logger.debug("Database connection test successful")
return True
except Exception as e:
logger.error(f"Database connection test failed: {e}")
return False
def __enter__(self):
"""Context manager entry."""
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Context manager exit."""
self.close_pool()
# Global connector instance
_connector = None
def get_connector():
"""Get or create global connector instance."""
global _connector
if _connector is None:
_connector = OracleConnector()
return _connector

299
db/repository.py Normal file
View File

@@ -0,0 +1,299 @@
#!/usr/bin/env python3
"""
Data access layer for NEFT inward file processing.
Handles CRUD operations and transaction management.
"""
from typing import List, Optional
from logging_config import get_logger
from .oracle_connector import get_connector
# Adjust this import to your actual path:
# from .models import NEFTInwardRecord, ProcessedFile
from .models import NEFTInwardRecord, ProcessedFile
logger = get_logger(__name__)
class Repository:
"""Data access layer for NEFT inward processing."""
def __init__(self):
"""Initialize repository with connector."""
self.connector = get_connector()
def bulk_insert_transactions(self, transactions: List[NEFTInwardRecord]) -> int:
"""
Bulk insert NEFT transactions into inward_neft_api_log.
Args:
transactions: List of NEFTInwardRecord objects
Returns:
Number of inserted rows
"""
if not transactions:
logger.warning("No transactions to insert")
return 0
conn = self.connector.get_connection()
cursor = None
try:
cursor = conn.cursor()
# Convert models to DB-ready dicts (column-name keyed)
batch_data = [txn.to_dict() for txn in transactions]
logger.info(batch_data)
insert_sql = """
INSERT INTO inward_neft_api_log (
TXNIND,
JRNL_ID,
REF_NO,
TXN_DATE,
TXN_AMT,
SENDER_IFSC,
RECIEVER_IFSC,
SENDER_ACCT_NO,
SENDER_ACCT_NAME,
REMITTER_DETAIL,
REMITTER_INFO,
RECVR_ACCT_NO,
RECVR_ACCT_NAME,
STATUS,
REJECT_CODE,
REJECT_REASON,
BENEF_ADDRESS,
MSG_TYPE
) VALUES (
:TXNIND,
:JRNL_ID,
:REF_NO,
:TXN_DATE,
:TXN_AMT,
:SENDER_IFSC,
:RECIEVER_IFSC,
:SENDER_ACCT_NO,
:SENDER_ACCT_NAME,
:REMITTER_DETAIL,
:REMITTER_INFO,
:RECVR_ACCT_NO,
:RECVR_ACCT_NAME,
:STATUS,
:REJECT_CODE,
:REJECT_REASON,
:BENEF_ADDRESS,
:MSG_TYPE
)
"""
cursor.executemany(insert_sql, batch_data)
conn.commit()
inserted_count = len(transactions)
logger.info(f"Inserted {inserted_count} NEFT transactions into inward_neft_api_log")
return inserted_count
except Exception as e:
if conn:
conn.rollback()
logger.error(f"Error inserting NEFT transactions: {e}", exc_info=True)
raise
finally:
if cursor:
cursor.close()
conn.close()
def is_file_processed(self, filename: str, bankcode: str) -> bool:
"""
Check if file has already been processed for a specific bank.
Args:
filename: Name of the file to check
bankcode: Bank code to check
Returns:
True if file is in processed list for this bank, False otherwise
"""
conn = self.connector.get_connection()
cursor = None
try:
cursor = conn.cursor()
cursor.execute(
"""
SELECT COUNT(*)
FROM neft_inward_processed_files
WHERE filename = :filename
AND bankcode = :bankcode
""",
{'filename': filename, 'bankcode': bankcode}
)
count = cursor.fetchone()[0]
return count > 0
except Exception as e:
logger.error(f"Error checking processed file: {e}", exc_info=True)
return False
finally:
if cursor:
cursor.close()
conn.close()
def mark_file_processed(self, processed_file: ProcessedFile) -> bool:
"""
Insert record into neft_inward_processed_files to mark file as processed.
Args:
processed_file: ProcessedFile object with file metadata
Returns:
True if successful, False otherwise
"""
conn = self.connector.get_connection()
cursor = None
try:
cursor = conn.cursor()
file_data = processed_file.to_dict()
insert_sql = """
INSERT INTO neft_inward_processed_files (
filename, bankcode, file_path, transaction_count,
status, error_message, processed_at
) VALUES (
:filename, :bankcode, :file_path, :transaction_count,
:status, :error_message, :processed_at
)
"""
cursor.execute(insert_sql, file_data)
conn.commit()
logger.info(f"Marked file as processed: {processed_file.filename}")
return True
except Exception as e:
if conn:
conn.rollback()
logger.error(f"Error marking file as processed: {e}", exc_info=True)
return False
finally:
if cursor:
cursor.close()
conn.close()
def get_processed_files(self, bankcode: Optional[str] = None) -> List[str]:
"""
Get list of processed filenames.
Args:
bankcode: Optional bankcode filter
Returns:
List of filenames that have been processed
"""
conn = self.connector.get_connection()
cursor = None
try:
cursor = conn.cursor()
if bankcode:
cursor.execute(
"""
SELECT filename
FROM neft_inward_processed_files
WHERE bankcode = :bankcode
ORDER BY processed_at DESC
""",
{'bankcode': bankcode}
)
else:
cursor.execute(
"""
SELECT filename
FROM neft_inward_processed_files
ORDER BY processed_at DESC
"""
)
filenames = [row[0] for row in cursor.fetchall()]
return filenames
except Exception as e:
logger.error(f"Error retrieving processed files: {e}", exc_info=True)
return []
finally:
if cursor:
cursor.close()
conn.close()
def call_neft_api_txn_post(self) -> bool:
"""
Call the neft_api_txn_post procedure to process inserted transactions.
Should be called once per processing cycle after all files are inserted.
Returns:
True if procedure executed successfully, False otherwise
"""
conn = self.connector.get_connection()
cursor = None
try:
cursor = conn.cursor()
logger.info("Calling neft_api_txn_post procedure to process all inserted transactions...")
# Prefer callproc if available
try:
cursor.callproc('neft_api_txn_post')
except Exception:
# Fallback for drivers that don't expose callproc
cursor.execute("BEGIN neft_api_txn_post; END;")
conn.commit()
logger.info("neft_api_txn_post procedure executed successfully")
return True
except Exception as e:
logger.error(f"Error calling neft_api_txn_post procedure: {e}", exc_info=True)
return False
finally:
if cursor:
cursor.close()
conn.close()
def verify_tables_exist(self):
"""
Verify that required database tables exist.
If tables are missing, terminate the program.
"""
conn = self.connector.get_connection()
cursor = None
try:
cursor = conn.cursor()
# Check if inward_neft_api_log table exists
try:
cursor.execute("SELECT COUNT(*) FROM inward_neft_api_log WHERE ROWNUM = 1")
logger.info("✓ inward_neft_api_log table exists")
except Exception as e:
logger.error(f"✗ inward_neft_api_log table not found: {e}")
raise SystemExit(
"FATAL: inward_neft_api_log table must be created manually before running this application"
)
# Check if neft_inward_processed_files table exists
try:
cursor.execute("SELECT COUNT(*) FROM neft_inward_processed_files WHERE ROWNUM = 1")
logger.info("✓ neft_inward_processed_files table exists")
except Exception as e:
logger.error(f"✗ neft_inward_processed_files table not found: {e}")
raise SystemExit(
"FATAL: neft_inward_processed_files table must be created manually before running this application"
)
logger.info("Database tables verified successfully")
except SystemExit:
raise
except Exception as e:
logger.error(f"Error verifying tables: {e}", exc_info=True)
raise SystemExit(f"FATAL: Error verifying database tables: {e}")
finally:
if cursor:
cursor.close()
conn.close()