first commit

This commit is contained in:
2026-06-13 14:58:21 +05:30
commit 6ec74f1c64
22 changed files with 1603 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
"""Database module for ACH file processing."""
from .oracle_connector import OracleConnector
from .repository import Repository
__all__ = ['OracleConnector', 'Repository']
+50
View File
@@ -0,0 +1,50 @@
from dataclasses import dataclass
from datetime import datetime
from decimal import Decimal
from typing import Optional
@dataclass
class UPIInwardRecord:
rrn: str
txn_date: str
txn_amt: Optional[Decimal]
credit_account: str
status: str
note: str
bank_code: str
txn_type: str # will be CREDIT / DEBIT from parser
def to_dict(self):
return {
"BANKCODE": self.bank_code,
"RRN": self.rrn,
"TXN_DATE": self.txn_date,
"TXN_AMT": self.txn_amt,
"TXN_TYPE": self.txn_type, # converted to CR before insert
"BENEFICIARY_ACCOUNT": self.credit_account,
"STATUS": self.status,
"NOTE": self.note,
}
@dataclass
class ProcessedFile:
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):
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
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
+200
View File
@@ -0,0 +1,200 @@
#!/usr/bin/env python3
"""
Data access layer for UPI 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
from .models import UPIInwardRecord, ProcessedFile
logger = get_logger(__name__)
class Repository:
"""Data access layer for UPI inward processing."""
def __init__(self):
self.connector = get_connector()
# ---------------------------------------------------------
# Account validation (same as NEFT)
# ---------------------------------------------------------
def validate_account_exists(self, account_number: str) -> bool:
if not account_number:
return False
last12 = str(account_number)[-12:]
conn = self.connector.get_connection()
try:
cursor = conn.cursor()
cursor.execute(
"SELECT COUNT(*) FROM dep_account WHERE link_accno = :accno",
{'accno': last12}
)
count = cursor.fetchone()[0]
return count > 0
except Exception as e:
logger.warning(f"Error validating account {account_number}: {e}")
return False
finally:
cursor.close()
conn.close()
# ---------------------------------------------------------
# UPDATED: UPI BULK INSERT WITH VALIDATION
# ---------------------------------------------------------
def bulk_insert_transactions(self, transactions: List[UPIInwardRecord]) -> tuple:
if not transactions:
logger.warning("No transactions to insert")
return 0, 0
valid_transactions = []
skipped_count = 0
for txn in transactions:
acct = txn.credit_account
# Condition 1: account exists
account_valid = self.validate_account_exists(acct)
# Condition 2: only CREDIT transactions
is_credit = (txn.txn_type == "CREDIT")
if account_valid and is_credit:
# Force DB value to CR
txn.txn_type = "CR"
valid_transactions.append(txn)
else:
skipped_count += 1
if not valid_transactions:
logger.info(f"All {skipped_count} transactions skipped")
return 0, skipped_count
conn = self.connector.get_connection()
cursor = None
try:
cursor = conn.cursor()
batch_data = [txn.to_dict() for txn in valid_transactions]
logger.info(batch_data)
insert_sql = """
INSERT INTO pacs_db.upi_inward_api_log (
BANKCODE,
RRN,
TXN_DATE,
TXN_AMT,
TXN_TYPE,
CREDIT_ACCOUNT,
STATUS,
NOTE
) VALUES (
:BANKCODE,
:RRN,
:TXN_DATE,
:TXN_AMT,
:TXN_TYPE,
:CREDIT_ACCOUNT,
:STATUS,
:NOTE
)
"""
cursor.executemany(insert_sql, batch_data)
logger.info(f"executemany rowcount: {cursor.rowcount}")
conn.commit()
logger.info("UPI transactions committed successfully")
inserted_count = len(valid_transactions)
logger.info(f"Inserted {inserted_count} UPI transactions")
return inserted_count, skipped_count
except Exception as e:
if conn:
conn.rollback()
logger.error(f"Error inserting UPI transactions: {e}", exc_info=True)
raise
finally:
if cursor:
cursor.close()
conn.close()
# ---------------------------------------------------------
# FILE TRACKING (UNCHANGED)
# ---------------------------------------------------------
def is_file_processed(self, filename: str, bankcode: str) -> bool:
conn = self.connector.get_connection()
cursor = None
try:
cursor = conn.cursor()
cursor.execute(
"""
SELECT COUNT(*)
FROM neft_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:
conn = self.connector.get_connection()
cursor = None
try:
cursor = conn.cursor()
file_data = processed_file.to_dict()
insert_sql = """
INSERT INTO neft_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()