From 34066cd367db996ec0b38a13f6d3370939fb3cc5 Mon Sep 17 00:00:00 2001 From: arpita-josh02 Date: Tue, 23 Sep 2025 13:03:59 +0530 Subject: [PATCH 01/17] banking api's --- service/README.md | 2 + service/banking/__init__.py: | 5 ++ service/banking/core_banking_routes.py | 101 +++++++++++++++++++++++++ service/banking/database.py | 31 ++++++++ service/banking/models.py | 81 ++++++++++++++++++++ service/main.py | 6 +- service/requirements.txt | 5 +- 7 files changed, 227 insertions(+), 4 deletions(-) create mode 100644 service/banking/__init__.py: create mode 100644 service/banking/core_banking_routes.py create mode 100644 service/banking/database.py create mode 100644 service/banking/models.py diff --git a/service/README.md b/service/README.md index 80bb3a3..27f7346 100644 --- a/service/README.md +++ b/service/README.md @@ -39,6 +39,8 @@ Activate the virtual environment Install dependencies pip install -r requirements.txt + + python3 -c "from database import Base, engine; import models; Base.metadata.create_all(bind=engine)" uvicorn main:app --host localhost --port 8000 --reload diff --git a/service/banking/__init__.py: b/service/banking/__init__.py: new file mode 100644 index 0000000..7e908ea --- /dev/null +++ b/service/banking/__init__.py: @@ -0,0 +1,5 @@ +# Initialize the banking package +from .database import Base, engine + +# Create tables when the package is imported +Base.metadata.create_all(bind=engine) \ No newline at end of file diff --git a/service/banking/core_banking_routes.py b/service/banking/core_banking_routes.py new file mode 100644 index 0000000..d70c24d --- /dev/null +++ b/service/banking/core_banking_routes.py @@ -0,0 +1,101 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from sqlalchemy import desc +from datetime import datetime +from .database import get_db +from .models import Customer, Account, Transaction +from pydantic import BaseModel + +router = APIRouter(prefix="/bank/me", tags=["banking"]) + +class PaymentRequest(BaseModel): + to: str + amount: float + +@router.get("/balance") +async def get_balance(customer_id: int = 1, db: Session = Depends(get_db)): + """Get balance for a customer account (default customer_id=1)""" + account = db.query(Account).filter( + Account.customer_id == customer_id, + Account.is_active == True + ).first() + + if not account: + raise HTTPException(status_code=404, detail="Account not found") + + return {"balance": account.balance} + +@router.post("/pay") +async def pay_money(request: PaymentRequest, customer_id: int = 1, db: Session = Depends(get_db)): + """Send money to a merchant or contact""" + to = request.to + amount = request.amount + + account = db.query(Account).filter( + Account.customer_id == customer_id, + Account.is_active == True + ).first() + + if not account: + raise HTTPException(status_code=404, detail="Account not found") + + if amount > account.balance: + raise HTTPException(status_code=400, detail="Insufficient balance") + + # Deduct and update balance + account.balance -= amount + + # Create a transaction record + reference_id = f"TXN-{datetime.now().strftime('%Y%m%d%H%M%S')}" + transaction = Transaction( + transaction_type="payment", + amount=amount, + merchant=to, + reference_id=reference_id, + from_account_id=account.id + ) + + db.add(transaction) + db.commit() + + return { + "message": "Payment successful", + "to": to, + "amount": amount, + "remaining_balance": account.balance + } + +@router.get("/transactions") +async def search_txn(merchant: str = None, limit: int = 5, customer_id: int = 1, db: Session = Depends(get_db)): + """Search transactions with optional merchant filter""" + account = db.query(Account).filter( + Account.customer_id == customer_id, + Account.is_active == True + ).first() + + if not account: + raise HTTPException(status_code=404, detail="Account not found") + + # Query transactions + query = db.query(Transaction).filter( + Transaction.from_account_id == account.id + ).order_by(desc(Transaction.transaction_date)) + + if merchant: + # Use the merchant field for filtering + query = query.filter(Transaction.merchant.ilike(f"%{merchant}%")) + + # Get transactions and format them like the mock + db_transactions = query.limit(limit).all() + + results = [ + { + "id": t.id, + "merchant": t.merchant, + "amount": -t.amount if t.transaction_type != "deposit" else t.amount, + "date": t.transaction_date.strftime("%Y-%m-%d") + } + for t in db_transactions + ] + + return {"transactions": results} \ No newline at end of file diff --git a/service/banking/database.py b/service/banking/database.py new file mode 100644 index 0000000..0819d58 --- /dev/null +++ b/service/banking/database.py @@ -0,0 +1,31 @@ +from sqlalchemy import create_engine, Column, Integer, String, Float, ForeignKey, DateTime, Text, Boolean, Index +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker, relationship +import datetime +import os + +# PostgreSQL connection string +DB_USER = os.getenv("DB_USER", "postgres") +DB_PASSWORD = os.getenv("DB_PASSWORD", "password") +DB_HOST = os.getenv("DB_HOST", "localhost") +DB_PORT = os.getenv("DB_PORT", "5432") +DB_NAME = os.getenv("DB_NAME", "voice_banking") + +SQLALCHEMY_DATABASE_URL = f"postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}" + +# Create PostgreSQL engine +engine = create_engine(SQLALCHEMY_DATABASE_URL) + +# Create a SessionLocal class for database session +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +# Create a Base class for declarative models +Base = declarative_base() + +# Dependency to get DB session +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() \ No newline at end of file diff --git a/service/banking/models.py b/service/banking/models.py new file mode 100644 index 0000000..68d0ef0 --- /dev/null +++ b/service/banking/models.py @@ -0,0 +1,81 @@ +from sqlalchemy import Column, Integer, String, Float, ForeignKey, DateTime, Text, Boolean +from sqlalchemy.orm import relationship +from .database import Base +import datetime + +class Customer(Base): + __tablename__ = "customers" + + id = Column(Integer, primary_key=True, index=True,autoincrement=True) + name = Column(String(100), nullable=False) + email = Column(String(100), unique=True, index=True) + phone = Column(String(20), unique=True, index=True) + address = Column(Text, nullable=True) + date_of_birth = Column(DateTime, nullable=True) + created_at = Column(DateTime, default=datetime.datetime.utcnow) + is_active = Column(Boolean, default=True) + + # Relationships + accounts = relationship("Account", back_populates="customer") + beneficiaries = relationship("Beneficiary", back_populates="customer") + + def __repr__(self): + return f"" + +class Account(Base): + __tablename__ = "accounts" + + id = Column(Integer, primary_key=True, index=True,autoincrement=True) + account_number = Column(String(20), unique=True, index=True) + account_type = Column(String(50)) # savings, current, etc. + balance = Column(Float, default=0.0) + currency = Column(String(3), default="INR") + customer_id = Column(Integer, ForeignKey("customers.id")) + created_at = Column(DateTime, default=datetime.datetime.utcnow) + is_active = Column(Boolean, default=True) + + # Relationships + customer = relationship("Customer", back_populates="accounts") + transactions_from = relationship("Transaction", back_populates="from_account", foreign_keys="Transaction.from_account_id") + transactions_to = relationship("Transaction", back_populates="to_account", foreign_keys="Transaction.to_account_id") + + def __repr__(self): + return f"" + +class Transaction(Base): + __tablename__ = "transactions" + + id = Column(Integer, primary_key=True, index=True,autoincrement=True) + transaction_type = Column(String(50)) # deposit, withdrawal, transfer + amount = Column(Float, nullable=False) + merchant = Column(String(100), nullable=True) # Added merchant field to match mock API + transaction_date = Column(DateTime, default=datetime.datetime.utcnow) + reference_id = Column(String(100), unique=True, index=True) + + # Account relationships + from_account_id = Column(Integer, ForeignKey("accounts.id")) + to_account_id = Column(Integer, ForeignKey("accounts.id"), nullable=True) + + # Back populates + from_account = relationship("Account", back_populates="transactions_from", foreign_keys=[from_account_id]) + to_account = relationship("Account", back_populates="transactions_to", foreign_keys=[to_account_id]) + + def __repr__(self): + return f"" + +class Beneficiary(Base): + __tablename__ = "beneficiaries" + + id = Column(Integer, primary_key=True, index=True,autoincrement=True) + name = Column(String(100), nullable=False) + account_number = Column(String(20), nullable=False) + bank_name = Column(String(100), nullable=True) + customer_id = Column(Integer, ForeignKey("customers.id")) + created_at = Column(DateTime, default=datetime.datetime.utcnow) + is_active = Column(Boolean, default=True) + + # Relationships + customer = relationship("Customer", back_populates="beneficiaries") + + def __repr__(self): + return f"" \ No newline at end of file diff --git a/service/main.py b/service/main.py index fbca279..c866ec4 100644 --- a/service/main.py +++ b/service/main.py @@ -14,7 +14,7 @@ from fastapi_versionizer.versionizer import Versionizer, api_version import json from core_banking_mock import router as core_banking_mock_router - +from banking.core_banking_routes import router as banking_router app = FastAPI() # Add CORS middleware to the application @@ -98,8 +98,8 @@ async def upload_audio(body: Body): sort_routes=True ).versionize() -app.include_router(core_banking_mock_router) - +# app.include_router(core_banking_mock_router) +app.include_router(banking_router) @app.post("/voice/transcribe-intent") async def transcribe_intent(audio: UploadFile = File(...), session_id: str = Form(...)): try: diff --git a/service/requirements.txt b/service/requirements.txt index e6de703..e1c5d64 100644 --- a/service/requirements.txt +++ b/service/requirements.txt @@ -26,4 +26,7 @@ dateparser torch transformers accelerate -sentencepiece \ No newline at end of file +sentencepiece + +sqlalchemy +psycopg2-binary \ No newline at end of file From e64cda41a8b7a392a84e7a226f6188a5881ba9a1 Mon Sep 17 00:00:00 2001 From: arpita-josh02 Date: Tue, 23 Sep 2025 15:30:24 +0530 Subject: [PATCH 02/17] changes request parm as per frontend --- service/banking/core_banking_routes.py | 50 +++++++++++++++----------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/service/banking/core_banking_routes.py b/service/banking/core_banking_routes.py index d70c24d..6f03f8c 100644 --- a/service/banking/core_banking_routes.py +++ b/service/banking/core_banking_routes.py @@ -5,6 +5,7 @@ from .database import get_db from .models import Customer, Account, Transaction from pydantic import BaseModel +from typing import Optional router = APIRouter(prefix="/bank/me", tags=["banking"]) @@ -66,28 +67,36 @@ async def pay_money(request: PaymentRequest, customer_id: int = 1, db: Session = } @router.get("/transactions") -async def search_txn(merchant: str = None, limit: int = 5, customer_id: int = 1, db: Session = Depends(get_db)): - """Search transactions with optional merchant filter""" - account = db.query(Account).filter( - Account.customer_id == customer_id, - Account.is_active == True - ).first() - - if not account: - raise HTTPException(status_code=404, detail="Account not found") - - # Query transactions - query = db.query(Transaction).filter( - Transaction.from_account_id == account.id - ).order_by(desc(Transaction.transaction_date)) - +async def search_txn( + merchant: str = None, + limit: int = None, + start_date: str = None, + end_date: str = None, + db: Session = Depends(get_db) +): + query = db.query(Transaction).order_by(desc(Transaction.transaction_date)) + if merchant: - # Use the merchant field for filtering query = query.filter(Transaction.merchant.ilike(f"%{merchant}%")) - - # Get transactions and format them like the mock - db_transactions = query.limit(limit).all() - + + if start_date: + try: + start_dt = datetime.datetime.strptime(start_date, "%Y-%m-%d") + query = query.filter(Transaction.transaction_date >= start_dt) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid start_date format. Use YYYY-MM-DD.") + if end_date: + try: + end_dt = datetime.datetime.strptime(end_date, "%Y-%m-%d") + datetime.timedelta(days=1) + query = query.filter(Transaction.transaction_date < end_dt) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid end_date format. Use YYYY-MM-DD.") + + if start_date or end_date: + db_transactions = query.all() + else: + db_transactions = query.limit(limit if limit else 5).all() + results = [ { "id": t.id, @@ -97,5 +106,4 @@ async def search_txn(merchant: str = None, limit: int = 5, customer_id: int = 1, } for t in db_transactions ] - return {"transactions": results} \ No newline at end of file From 674ab42dcc66675ae30062b42d3525a04669ee5f Mon Sep 17 00:00:00 2001 From: arpita-josh02 Date: Tue, 23 Sep 2025 15:52:35 +0530 Subject: [PATCH 03/17] new changes --- service/banking/core_banking_routes.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/service/banking/core_banking_routes.py b/service/banking/core_banking_routes.py index 6f03f8c..eab9174 100644 --- a/service/banking/core_banking_routes.py +++ b/service/banking/core_banking_routes.py @@ -1,7 +1,7 @@ from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from sqlalchemy import desc -from datetime import datetime +from datetime import datetime, timedelta from .database import get_db from .models import Customer, Account, Transaction from pydantic import BaseModel @@ -68,6 +68,7 @@ async def pay_money(request: PaymentRequest, customer_id: int = 1, db: Session = @router.get("/transactions") async def search_txn( + customer_id: int = None, merchant: str = None, limit: int = None, start_date: str = None, @@ -76,23 +77,39 @@ async def search_txn( ): query = db.query(Transaction).order_by(desc(Transaction.transaction_date)) + # Filter by customer_id if provided + if customer_id is not None: + # Find all accounts for this customer + accounts = db.query(Account.id).filter(Account.customer_id == customer_id, Account.is_active == True).all() + account_ids = [a.id for a in accounts] + if account_ids: + query = query.filter(Transaction.from_account_id.in_(account_ids)) + else: + return {"transactions": []} + + # Filter by merchant if provided if merchant: query = query.filter(Transaction.merchant.ilike(f"%{merchant}%")) + # Filter by date range if provided + date_filtered = False if start_date: try: - start_dt = datetime.datetime.strptime(start_date, "%Y-%m-%d") + start_dt = datetime.strptime(start_date, "%Y-%m-%d") query = query.filter(Transaction.transaction_date >= start_dt) + date_filtered = True except ValueError: raise HTTPException(status_code=400, detail="Invalid start_date format. Use YYYY-MM-DD.") if end_date: try: - end_dt = datetime.datetime.strptime(end_date, "%Y-%m-%d") + datetime.timedelta(days=1) + end_dt = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1) query = query.filter(Transaction.transaction_date < end_dt) + date_filtered = True except ValueError: raise HTTPException(status_code=400, detail="Invalid end_date format. Use YYYY-MM-DD.") - if start_date or end_date: + # If date filtering is applied, ignore limit + if date_filtered: db_transactions = query.all() else: db_transactions = query.limit(limit if limit else 5).all() From 5756af462f35476c6882c3f485a03365b4de20c6 Mon Sep 17 00:00:00 2001 From: arpita-josh02 Date: Tue, 23 Sep 2025 17:23:41 +0530 Subject: [PATCH 04/17] remove default cred from file take from env --- service/banking/database.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/service/banking/database.py b/service/banking/database.py index 0819d58..d3ba3e0 100644 --- a/service/banking/database.py +++ b/service/banking/database.py @@ -5,11 +5,11 @@ import os # PostgreSQL connection string -DB_USER = os.getenv("DB_USER", "postgres") -DB_PASSWORD = os.getenv("DB_PASSWORD", "password") -DB_HOST = os.getenv("DB_HOST", "localhost") -DB_PORT = os.getenv("DB_PORT", "5432") -DB_NAME = os.getenv("DB_NAME", "voice_banking") +DB_USER = os.getenv("DB_USER") +DB_PASSWORD = os.getenv("DB_PASSWORD") +DB_HOST = os.getenv("DB_HOST") +DB_PORT = os.getenv("DB_PORT") +DB_NAME = os.getenv("DB_NAME") SQLALCHEMY_DATABASE_URL = f"postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}" From b98a8001f236298958d708a41490c6d262be47a0 Mon Sep 17 00:00:00 2001 From: arpita-josh02 Date: Tue, 23 Sep 2025 17:38:20 +0530 Subject: [PATCH 05/17] move database cred to config file --- service/banking/database.py | 12 +++++++----- service/config.py | 5 +++++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/service/banking/database.py b/service/banking/database.py index d3ba3e0..99bfab1 100644 --- a/service/banking/database.py +++ b/service/banking/database.py @@ -3,13 +3,15 @@ from sqlalchemy.orm import sessionmaker, relationship import datetime import os +import config # PostgreSQL connection string -DB_USER = os.getenv("DB_USER") -DB_PASSWORD = os.getenv("DB_PASSWORD") -DB_HOST = os.getenv("DB_HOST") -DB_PORT = os.getenv("DB_PORT") -DB_NAME = os.getenv("DB_NAME") +DB_USER = config.db_user +DB_PASSWORD = config.db_password +DB_HOST = config.db_host +DB_PORT = config.db_port +DB_NAME = config.db_name + SQLALCHEMY_DATABASE_URL = f"postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}" diff --git a/service/config.py b/service/config.py index 8bba447..f619c5e 100644 --- a/service/config.py +++ b/service/config.py @@ -11,3 +11,8 @@ ollama_model_name = os.getenv("OLLAMA_MODEL_NAME", "llama3.2") open_ai_model_name = os.getenv("OPENAI_MODEL_NAME", "gpt-4") open_ai_temperature = os.getenv("OPENAI_TEMPERATURE", 0.2) +db_user = os.getenv("DB_USER") +db_password = os.getenv("DB_PASSWORD") +db_host = os.getenv("DB_HOST") +db_port = os.getenv("DB_PORT") +db_name = os.getenv("DB_NAME") \ No newline at end of file From b78de13d6d34fd98e45e7c8ba3897e8d7259d89d Mon Sep 17 00:00:00 2001 From: arpita-josh02 Date: Wed, 24 Sep 2025 16:33:49 +0530 Subject: [PATCH 06/17] upadted chnages --- service/README.md | 4 +- service/banking/core_banking_routes.py | 152 ++++++++++++++++------ service/banking/database.py | 6 +- service/banking/models.py | 14 +- service/banking/seed_data.py | 171 +++++++++++++++++++++++++ 5 files changed, 299 insertions(+), 48 deletions(-) create mode 100644 service/banking/seed_data.py diff --git a/service/README.md b/service/README.md index 27f7346..c8810e6 100644 --- a/service/README.md +++ b/service/README.md @@ -40,8 +40,10 @@ Install dependencies pip install -r requirements.txt - python3 -c "from database import Base, engine; import models; Base.metadata.create_all(bind=engine)" + python3 -c "from banking.database import Base, engine; import banking.models; Base.metadata.create_all(bind=engine)" + python3 banking/seed_data.py + uvicorn main:app --host localhost --port 8000 --reload # Api endpoints diff --git a/service/banking/core_banking_routes.py b/service/banking/core_banking_routes.py index eab9174..2a0de2b 100644 --- a/service/banking/core_banking_routes.py +++ b/service/banking/core_banking_routes.py @@ -1,9 +1,9 @@ -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, HTTPException, Response, status from sqlalchemy.orm import Session from sqlalchemy import desc from datetime import datetime, timedelta from .database import get_db -from .models import Customer, Account, Transaction +from .models import Customer, Account, Transaction, Beneficiary from pydantic import BaseModel from typing import Optional @@ -12,74 +12,157 @@ class PaymentRequest(BaseModel): to: str amount: float + transaction_type: Optional[str] = None + payment_method: Optional[str] = None + category: Optional[str] = None + @router.get("/balance") -async def get_balance(customer_id: int = 1, db: Session = Depends(get_db)): - """Get balance for a customer account (default customer_id=1)""" +async def get_balance( + customer_id: int = None, + phone: str = None, + db: Session = Depends(get_db) +): + """Get balance for a customer account (by customer_id or phone)""" + if phone: + customer = db.query(Customer).filter(Customer.phone == phone, Customer.is_active == True).first() + if not customer: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Customer with this phone not found") + customer_id = customer.id + + if not customer_id: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="customer_id or phone required") + account = db.query(Account).filter( Account.customer_id == customer_id, Account.is_active == True ).first() - + if not account: - raise HTTPException(status_code=404, detail="Account not found") - + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Account not found") + return {"balance": account.balance} + @router.post("/pay") -async def pay_money(request: PaymentRequest, customer_id: int = 1, db: Session = Depends(get_db)): +async def pay_money( + request: PaymentRequest, + customer_id: int = None, + phone: str = None, + db: Session = Depends(get_db) +): """Send money to a merchant or contact""" + + # ✅ Step 1: Resolve customer_id + if phone: + customer = db.query(Customer).filter(Customer.phone == phone, Customer.is_active == True).first() + if not customer: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Customer with this phone not found") + customer_id = customer.id + + if not customer_id: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="customer_id or phone required") + + # ✅ Step 2: Extract request body values to = request.to amount = request.amount + transaction_type = request.transaction_type + payment_method = request.payment_method + category = request.category + + # ✅ Step 3: Check beneficiary existence + beneficiaries = db.query(Beneficiary).filter( + Beneficiary.customer_id == customer_id, + ( + (Beneficiary.name.ilike(f"%{to}%")) | + (Beneficiary.nickname.ilike(f"%{to}%")) | + (Beneficiary.tag.ilike(f"%{to}%")) + ) + ).all() + + if len(beneficiaries) == 0: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="No matching beneficiary found") + if len(beneficiaries) > 1: + raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="More than one matching beneficiary found") + + beneficiary = beneficiaries[0] # ✅ single match + + # ✅ Step 4: Category classification + food_merchants = ["swiggy", "zomato", "restaurant"] + ecommerce_merchants = ["amazon", "myntra", "flipkart"] + utility_merchants = ["electricity", "water", "gas", "mobile"] + + if category: + category_lower = category.lower() + if any(m in category_lower for m in food_merchants): + category = "food" + elif any(m in category_lower for m in ecommerce_merchants): + category = "e-commerce" + elif any(m in category_lower for m in utility_merchants): + category = "utility" + else: + category = "individual" + else: + category = "individual" + # ✅ Step 5: Get customer account account = db.query(Account).filter( Account.customer_id == customer_id, Account.is_active == True ).first() if not account: - raise HTTPException(status_code=404, detail="Account not found") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Account not found") if amount > account.balance: - raise HTTPException(status_code=400, detail="Insufficient balance") + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Insufficient balance") - # Deduct and update balance + # ✅ Step 6: Deduct balance account.balance -= amount - - # Create a transaction record + + # ✅ Step 7: Create transaction record reference_id = f"TXN-{datetime.now().strftime('%Y%m%d%H%M%S')}" transaction = Transaction( - transaction_type="payment", + transaction_type=transaction_type, amount=amount, - merchant=to, + recipient=to, # ✅ store beneficiary name reference_id=reference_id, - from_account_id=account.id + payment_method=payment_method , + category=category, + from_account_id=account.id, # ✅ link with sender account ) - + db.add(transaction) db.commit() return { - "message": "Payment successful", + "status": "success", "to": to, "amount": amount, - "remaining_balance": account.balance + "balance": account.balance } + @router.get("/transactions") async def search_txn( customer_id: int = None, + phone: str = None, merchant: str = None, limit: int = None, start_date: str = None, end_date: str = None, db: Session = Depends(get_db) ): + if phone: + customer = db.query(Customer).filter(Customer.phone == phone, Customer.is_active == True).first() + if not customer: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Customer with this phone not found") + customer_id = customer.id + query = db.query(Transaction).order_by(desc(Transaction.transaction_date)) # Filter by customer_id if provided if customer_id is not None: - # Find all accounts for this customer accounts = db.query(Account.id).filter(Account.customer_id == customer_id, Account.is_active == True).all() account_ids = [a.id for a in accounts] if account_ids: @@ -89,38 +172,23 @@ async def search_txn( # Filter by merchant if provided if merchant: - query = query.filter(Transaction.merchant.ilike(f"%{merchant}%")) + query = query.filter(Transaction.recipient.ilike(f"%{merchant}%")) # Filter by date range if provided - date_filtered = False if start_date: try: start_dt = datetime.strptime(start_date, "%Y-%m-%d") query = query.filter(Transaction.transaction_date >= start_dt) - date_filtered = True except ValueError: - raise HTTPException(status_code=400, detail="Invalid start_date format. Use YYYY-MM-DD.") + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid start_date format. Use YYYY-MM-DD.") if end_date: try: end_dt = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1) query = query.filter(Transaction.transaction_date < end_dt) - date_filtered = True except ValueError: - raise HTTPException(status_code=400, detail="Invalid end_date format. Use YYYY-MM-DD.") + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid end_date format. Use YYYY-MM-DD.") - # If date filtering is applied, ignore limit - if date_filtered: - db_transactions = query.all() - else: - db_transactions = query.limit(limit if limit else 5).all() - - results = [ - { - "id": t.id, - "merchant": t.merchant, - "amount": -t.amount if t.transaction_type != "deposit" else t.amount, - "date": t.transaction_date.strftime("%Y-%m-%d") - } - for t in db_transactions - ] - return {"transactions": results} \ No newline at end of file + # If dates provided, ignore limit + db_transactions = query.limit(limit if not (start_date or end_date) else None).all() + + return {"transactions": db_transactions} diff --git a/service/banking/database.py b/service/banking/database.py index 99bfab1..817c988 100644 --- a/service/banking/database.py +++ b/service/banking/database.py @@ -3,7 +3,11 @@ from sqlalchemy.orm import sessionmaker, relationship import datetime import os -import config +import sys + +# Add the parent directory to sys.path +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +import config # Direct import from parent directory # PostgreSQL connection string DB_USER = config.db_user diff --git a/service/banking/models.py b/service/banking/models.py index 68d0ef0..b23d588 100644 --- a/service/banking/models.py +++ b/service/banking/models.py @@ -1,6 +1,6 @@ from sqlalchemy import Column, Integer, String, Float, ForeignKey, DateTime, Text, Boolean from sqlalchemy.orm import relationship -from .database import Base +from banking.database import Base import datetime class Customer(Base): @@ -31,6 +31,8 @@ class Account(Base): balance = Column(Float, default=0.0) currency = Column(String(3), default="INR") customer_id = Column(Integer, ForeignKey("customers.id")) + branch = Column(String(100), nullable=True) # Added field + ifsc_code = Column(String(20), nullable=True) # Added field created_at = Column(DateTime, default=datetime.datetime.utcnow) is_active = Column(Boolean, default=True) @@ -46,12 +48,13 @@ class Transaction(Base): __tablename__ = "transactions" id = Column(Integer, primary_key=True, index=True,autoincrement=True) - transaction_type = Column(String(50)) # deposit, withdrawal, transfer + transaction_type = Column(String(50)) # credit,debit amount = Column(Float, nullable=False) - merchant = Column(String(100), nullable=True) # Added merchant field to match mock API + recipient = Column(String(100), nullable=True) # Added merchant field to match mock API transaction_date = Column(DateTime, default=datetime.datetime.utcnow) reference_id = Column(String(100), unique=True, index=True) - + category = Column(String(100), nullable=True) + payment_method = Column(String(50), nullable=True) #upi,rtgs,neft,cash,card,imps # Account relationships from_account_id = Column(Integer, ForeignKey("accounts.id")) to_account_id = Column(Integer, ForeignKey("accounts.id"), nullable=True) @@ -71,6 +74,9 @@ class Beneficiary(Base): account_number = Column(String(20), nullable=False) bank_name = Column(String(100), nullable=True) customer_id = Column(Integer, ForeignKey("customers.id")) + nickname = Column(String(100), nullable=True) # Added field + tag = Column(String(50), nullable=True) # Added field + ifsc_code = Column(String(20), nullable=True) # Added field created_at = Column(DateTime, default=datetime.datetime.utcnow) is_active = Column(Boolean, default=True) diff --git a/service/banking/seed_data.py b/service/banking/seed_data.py new file mode 100644 index 0000000..495d62f --- /dev/null +++ b/service/banking/seed_data.py @@ -0,0 +1,171 @@ +from database import SessionLocal +from models import Customer, Account, Transaction, Beneficiary +import datetime +import random + +db = SessionLocal() + +# 1. Create Customers +customers = [ + Customer( + name="Amit Sharma", + email="amit.sharma@example.com", + phone="9876543210", + address="Mumbai, India", + date_of_birth=datetime.datetime(1990, 5, 21), + is_active=True + ), + Customer( + name="Priya Singh", + email="priya.singh@example.com", + phone="9123456780", + address="Delhi, India", + date_of_birth=datetime.datetime(1988, 8, 15), + is_active=True + ), + Customer( + name="Rahul Verma", + email="rahul.verma@example.com", + phone="9988776655", + address="Bangalore, India", + date_of_birth=datetime.datetime(1992, 2, 10), + is_active=True + ), +] +db.add_all(customers) +db.commit() + +# 2. Create Accounts +accounts = [ + Account( + account_number="AMIT12345", + account_type="savings", + balance=15000.0, + currency="INR", + branch="Mumbai Main", + ifsc_code="SBIN0000123", + customer_id=customers[0].id, + is_active=True + ), + Account( + account_number="PRIYA54321", + account_type="savings", + balance=22000.0, + currency="INR", + branch="Delhi Central", + ifsc_code="SBIN0000456", + customer_id=customers[1].id, + is_active=True + ), + Account( + account_number="RAHUL67890", + account_type="current", + balance=30500.0, + currency="INR", + branch="Bangalore City", + ifsc_code="SBIN0000789", + customer_id=customers[2].id, + is_active=True + ), +] +db.add_all(accounts) +db.commit() + +# 3. Create Transactions (10 per customer, random dates in last 3 months) +merchant_list = [ + ("swiggy", "food"), ("zomato", "food"), ("amazon", "e-commerce"), + ("flipkart", "e-commerce"), ("electricity", "utility"), ("water", "utility"), + ("gas", "utility"), ("restaurant", "food"), ("myntra", "e-commerce"), ("mobile", "utility") +] + +transactions = [] +for i, account in enumerate(accounts): + for j in range(10): + merchant, category = random.choice(merchant_list) + txn_date = datetime.datetime(2024, 7, 6) + datetime.timedelta(days=random.randint(0, 92)) + txn = Transaction( + transaction_type="debit", + amount=random.randint(500, 3000), + recipient=merchant, + transaction_date=txn_date, + reference_id=f"TXN-{i+1}-{j+1}-{txn_date.strftime('%Y%m%d')}", + category=category, + payment_method="upi", + from_account_id=account.id + ) + transactions.append(txn) +db.add_all(transactions) +db.commit() + +# 4. Create Beneficiaries (5 with duplicate name "Shailesh") +# 4. Create Beneficiaries (3 named "Shailesh" + 3 different names) +beneficiaries = [ + # 3 duplicates - Shailesh + Beneficiary( + name="Shailesh", + account_number="SHL1001", + bank_name="SBI", + customer_id=customers[0].id, + nickname="shailesh1", + tag="friend", + ifsc_code="SBIN0000123", + is_active=True + ), + Beneficiary( + name="Shailesh", + account_number="SHL1002", + bank_name="HDFC", + customer_id=customers[0].id, + nickname="shailesh2", + tag="colleague", + ifsc_code="HDFC0000456", + is_active=True + ), + Beneficiary( + name="Shailesh", + account_number="SHL1003", + bank_name="ICICI", + customer_id=customers[1].id, + nickname="shailesh3", + tag="family", + ifsc_code="ICIC0000789", + is_active=True + ), + + # 3 unique names + Beneficiary( + name="Ramesh Kumar", + account_number="RMK2001", + bank_name="Axis Bank", + customer_id=customers[1].id, + nickname="ramesh", + tag="friend", + ifsc_code="UTIB0000123", + is_active=True + ), + Beneficiary( + name="Suresh Patil", + account_number="SRP2002", + bank_name="Kotak Mahindra", + customer_id=customers[2].id, + nickname="suresh", + tag="business", + ifsc_code="KKBK0000456", + is_active=True + ), + Beneficiary( + name="Anita Desai", + account_number="ANT2003", + bank_name="Yes Bank", + customer_id=customers[0].id, + nickname="anita", + tag="family", + ifsc_code="YESB0000789", + is_active=True + ), +] + +db.add_all(beneficiaries) +db.commit() +print("Seed data inserted successfully!") +db.close() \ No newline at end of file From 681bbd97d5f8c20375023956dbc5bb6bfe438c26 Mon Sep 17 00:00:00 2001 From: arpita-josh02 Date: Thu, 25 Sep 2025 12:28:57 +0530 Subject: [PATCH 07/17] added details message in response --- service/banking/core_banking_routes.py | 104 +++++++++++++++++-------- 1 file changed, 72 insertions(+), 32 deletions(-) diff --git a/service/banking/core_banking_routes.py b/service/banking/core_banking_routes.py index 2a0de2b..0261c8a 100644 --- a/service/banking/core_banking_routes.py +++ b/service/banking/core_banking_routes.py @@ -16,7 +16,6 @@ class PaymentRequest(BaseModel): payment_method: Optional[str] = None category: Optional[str] = None - @router.get("/balance") async def get_balance( customer_id: int = None, @@ -25,13 +24,19 @@ async def get_balance( ): """Get balance for a customer account (by customer_id or phone)""" if phone: - customer = db.query(Customer).filter(Customer.phone == phone, Customer.is_active == True).first() + customer = db.query(Customer).filter(Customer.phone == phone).first() if not customer: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Customer with this phone not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Customer with phone number '{phone}' not found " + ) customer_id = customer.id if not customer_id: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="customer_id or phone required") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Missing required parameter: Please provide either 'customer_id' or 'phone'" + ) account = db.query(Account).filter( Account.customer_id == customer_id, @@ -39,7 +44,10 @@ async def get_balance( ).first() if not account: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Account not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"No active account found for customer ID {customer_id}" + ) return {"balance": account.balance} @@ -53,24 +61,27 @@ async def pay_money( ): """Send money to a merchant or contact""" - # ✅ Step 1: Resolve customer_id if phone: - customer = db.query(Customer).filter(Customer.phone == phone, Customer.is_active == True).first() + customer = db.query(Customer).filter(Customer.phone == phone).first() if not customer: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Customer with this phone not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Customer with phone number '{phone}' not found " + ) customer_id = customer.id if not customer_id: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="customer_id or phone required") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Missing required parameter: Please provide either 'customer_id' or 'phone'" + ) - # ✅ Step 2: Extract request body values to = request.to amount = request.amount transaction_type = request.transaction_type payment_method = request.payment_method category = request.category - # ✅ Step 3: Check beneficiary existence beneficiaries = db.query(Beneficiary).filter( Beneficiary.customer_id == customer_id, ( @@ -81,13 +92,19 @@ async def pay_money( ).all() if len(beneficiaries) == 0: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="No matching beneficiary found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"No beneficiary matching '{to}' found for this customer. Please check the name or add as a new beneficiary." + ) if len(beneficiaries) > 1: - raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="More than one matching beneficiary found") + beneficiary_names = [b.name for b in beneficiaries] + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=f"Multiple matching beneficiaries found for '{to}': {', '.join(beneficiary_names)}. Please use a more specific name." + ) - beneficiary = beneficiaries[0] # ✅ single match + beneficiary = beneficiaries[0] - # ✅ Step 4: Category classification food_merchants = ["swiggy", "zomato", "restaurant"] ecommerce_merchants = ["amazon", "myntra", "flipkart"] utility_merchants = ["electricity", "water", "gas", "mobile"] @@ -105,31 +122,34 @@ async def pay_money( else: category = "individual" - # ✅ Step 5: Get customer account account = db.query(Account).filter( Account.customer_id == customer_id, Account.is_active == True ).first() if not account: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Account not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"No active account found for customer ID {customer_id}" + ) if amount > account.balance: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Insufficient balance") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Insufficient balance: Required ₹{amount:.2f}, available balance ₹{account.balance:.2f}" + ) - # ✅ Step 6: Deduct balance account.balance -= amount - # ✅ Step 7: Create transaction record reference_id = f"TXN-{datetime.now().strftime('%Y%m%d%H%M%S')}" transaction = Transaction( transaction_type=transaction_type, amount=amount, - recipient=to, # ✅ store beneficiary name + recipient=to, reference_id=reference_id, - payment_method=payment_method , + payment_method=payment_method, category=category, - from_account_id=account.id, # ✅ link with sender account + from_account_id=account.id, ) db.add(transaction) @@ -147,32 +167,46 @@ async def pay_money( async def search_txn( customer_id: int = None, phone: str = None, - merchant: str = None, + recipient: str = None, + category: str = None, limit: int = None, start_date: str = None, end_date: str = None, db: Session = Depends(get_db) ): if phone: - customer = db.query(Customer).filter(Customer.phone == phone, Customer.is_active == True).first() + customer = db.query(Customer).filter( + Customer.phone == phone, + Customer.is_active == True + ).first() if not customer: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Customer with this phone not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Customer with phone number '{phone}' not found or is inactive" + ) customer_id = customer.id query = db.query(Transaction).order_by(desc(Transaction.transaction_date)) # Filter by customer_id if provided if customer_id is not None: - accounts = db.query(Account.id).filter(Account.customer_id == customer_id, Account.is_active == True).all() + accounts = db.query(Account.id).filter( + Account.customer_id == customer_id, + Account.is_active == True + ).all() account_ids = [a.id for a in accounts] if account_ids: query = query.filter(Transaction.from_account_id.in_(account_ids)) else: return {"transactions": []} - # Filter by merchant if provided - if merchant: - query = query.filter(Transaction.recipient.ilike(f"%{merchant}%")) + # Filter by recipient if provided + if recipient: + query = query.filter(Transaction.recipient.ilike(f"%{recipient}%")) + + # Filter by category if provided + if category: + query = query.filter(Transaction.category.ilike(f"%{category}%")) # Filter by date range if provided if start_date: @@ -180,13 +214,19 @@ async def search_txn( start_dt = datetime.strptime(start_date, "%Y-%m-%d") query = query.filter(Transaction.transaction_date >= start_dt) except ValueError: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid start_date format. Use YYYY-MM-DD.") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Invalid start_date format '{start_date}'. Please use YYYY-MM-DD format (e.g., 2025-09-25)." + ) if end_date: try: end_dt = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1) query = query.filter(Transaction.transaction_date < end_dt) except ValueError: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid end_date format. Use YYYY-MM-DD.") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Invalid end_date format '{end_date}'. Please use YYYY-MM-DD format (e.g., 2025-09-25)." + ) # If dates provided, ignore limit db_transactions = query.limit(limit if not (start_date or end_date) else None).all() From 4e4210d928e8ecf8755f620070ff4b14e5524b04 Mon Sep 17 00:00:00 2001 From: arpita-josh02 Date: Thu, 25 Sep 2025 12:32:12 +0530 Subject: [PATCH 08/17] added customer id in api response --- service/banking/core_banking_routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/banking/core_banking_routes.py b/service/banking/core_banking_routes.py index 0261c8a..d4e6ab6 100644 --- a/service/banking/core_banking_routes.py +++ b/service/banking/core_banking_routes.py @@ -49,7 +49,7 @@ async def get_balance( detail=f"No active account found for customer ID {customer_id}" ) - return {"balance": account.balance} + return {"balance": account.balance,"customer_id":customer_id} @router.post("/pay") From 692406f28ea80a92c9d3b3db61efaa8dd26524fc Mon Sep 17 00:00:00 2001 From: arpita-josh02 Date: Thu, 25 Sep 2025 15:57:05 +0530 Subject: [PATCH 09/17] modify seed data and response of benificary --- service/banking/core_banking_routes.py | 155 +++++++++++++++++++------ service/banking/seed_data.py | 88 +++++++++----- 2 files changed, 180 insertions(+), 63 deletions(-) diff --git a/service/banking/core_banking_routes.py b/service/banking/core_banking_routes.py index d4e6ab6..9e96514 100644 --- a/service/banking/core_banking_routes.py +++ b/service/banking/core_banking_routes.py @@ -52,6 +52,105 @@ async def get_balance( return {"balance": account.balance,"customer_id":customer_id} +def resolve_conflict(to: str, matches, primary_field: str): + """ + Smart conflict resolution: + - If primary_field matches multiple entries: + - Check if nicknames differ -> show nickname differences + - Else check if tags differ -> show tag differences + - Else show generic message + """ + # Check nickname differences + nicknames = [b.nickname for b in matches if b.nickname] + unique_nicknames = set(nicknames) + if len(unique_nicknames) > 1: + details = [f"'{b.nickname}'" for b in matches if b.nickname] + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=f"Multiple beneficiaries found with {primary_field} '{to}' with different nicknames: {', '.join(details)}. Please specify which nickname." + ) + + # Nicknames same or missing, check tags + tags = [b.tag for b in matches if b.tag] + unique_tags = set(tags) + if len(unique_tags) > 1: + details = [f"'{b.tag}'" for b in matches if b.tag] + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=f"Multiple beneficiaries found with {primary_field} '{to}' with same nickname but different tags: {', '.join(details)}. Please specify which tag." + ) + + # No distinguishing nicknames or tags + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=f"Multiple beneficiaries found with {primary_field} '{to}' with no distinguishing nickname or tag. Please use a more specific identifier." + ) + + +def find_beneficiary(db: Session, customer_id: int, to: str): + """Find a beneficiary by name, nickname, or tag with conflict handling.""" + for field in ["name", "nickname", "tag"]: + matches = db.query(Beneficiary).filter( + Beneficiary.customer_id == customer_id, + getattr(Beneficiary, field).ilike(to) + ).all() + + if matches: + if len(matches) == 1: + return matches[0] + + # Smart conflict resolution only for name + if field == "name": + resolve_conflict(to, matches, primary_field="name") + else: + # Fallback for nickname or tag + details = [] + for b in matches: + identifier = f"'{b.name}'" + if b.nickname: + identifier += f" (nickname: {b.nickname})" + if b.tag: + identifier += f" (tag: {b.tag})" + details.append(identifier) + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=f"Multiple beneficiaries found with {field} '{to}': {', '.join(details)}. Please specify further." + ) + + # Partial match fallback + matches = db.query(Beneficiary).filter( + Beneficiary.customer_id == customer_id, + ( + Beneficiary.name.ilike(f"%{to}%") | + Beneficiary.nickname.ilike(f"%{to}%") | + Beneficiary.tag.ilike(f"%{to}%") + ) + ).all() + + if not matches: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"No beneficiary matching '{to}' found for this customer. " + f"Please check the name, nickname, or tag, or add as a new beneficiary." + ) + + if len(matches) > 1: + details = [] + for b in matches: + identifier = f"'{b.name}'" + if b.nickname: + identifier += f" (nickname: {b.nickname})" + if b.tag: + identifier += f" (tag: {b.tag})" + details.append(identifier) + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=f"Multiple beneficiaries partially match '{to}': {', '.join(details)}. Please use a more specific name, nickname, or tag." + ) + + return matches[0] + + @router.post("/pay") async def pay_money( request: PaymentRequest, @@ -61,50 +160,32 @@ async def pay_money( ): """Send money to a merchant or contact""" + # Identify customer if phone: customer = db.query(Customer).filter(Customer.phone == phone).first() if not customer: raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Customer with phone number '{phone}' not found " + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Customer with phone number '{phone}' not found" ) customer_id = customer.id if not customer_id: raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, + status_code=status.HTTP_400_BAD_REQUEST, detail="Missing required parameter: Please provide either 'customer_id' or 'phone'" ) to = request.to amount = request.amount transaction_type = request.transaction_type - payment_method = request.payment_method + payment_method = request.payment_method or "upi" category = request.category - beneficiaries = db.query(Beneficiary).filter( - Beneficiary.customer_id == customer_id, - ( - (Beneficiary.name.ilike(f"%{to}%")) | - (Beneficiary.nickname.ilike(f"%{to}%")) | - (Beneficiary.tag.ilike(f"%{to}%")) - ) - ).all() - - if len(beneficiaries) == 0: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"No beneficiary matching '{to}' found for this customer. Please check the name or add as a new beneficiary." - ) - if len(beneficiaries) > 1: - beneficiary_names = [b.name for b in beneficiaries] - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail=f"Multiple matching beneficiaries found for '{to}': {', '.join(beneficiary_names)}. Please use a more specific name." - ) - - beneficiary = beneficiaries[0] + # Resolve beneficiary + beneficiary = find_beneficiary(db, customer_id, to) + # Categorize merchant food_merchants = ["swiggy", "zomato", "restaurant"] ecommerce_merchants = ["amazon", "myntra", "flipkart"] utility_merchants = ["electricity", "water", "gas", "mobile"] @@ -122,6 +203,7 @@ async def pay_money( else: category = "individual" + # Find active account account = db.query(Account).filter( Account.customer_id == customer_id, Account.is_active == True @@ -129,27 +211,31 @@ async def pay_money( if not account: raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, + status_code=status.HTTP_404_NOT_FOUND, detail=f"No active account found for customer ID {customer_id}" ) + # Balance check if amount > account.balance: raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Insufficient balance: Required ₹{amount:.2f}, available balance ₹{account.balance:.2f}" + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Insufficient balance: Required ₹{amount:.2f}, " + f"available balance ₹{account.balance:.2f}" ) + # Deduct balance account.balance -= amount + # Create transaction reference_id = f"TXN-{datetime.now().strftime('%Y%m%d%H%M%S')}" transaction = Transaction( transaction_type=transaction_type, amount=amount, - recipient=to, + recipient=beneficiary.name, reference_id=reference_id, payment_method=payment_method, category=category, - from_account_id=account.id, + from_account_id=account.id, ) db.add(transaction) @@ -157,9 +243,12 @@ async def pay_money( return { "status": "success", - "to": to, + "to": beneficiary.name, "amount": amount, - "balance": account.balance + "balance": account.balance, + "reference_id": reference_id, + "payment_method": payment_method, + "category": category } diff --git a/service/banking/seed_data.py b/service/banking/seed_data.py index 495d62f..3d484a7 100644 --- a/service/banking/seed_data.py +++ b/service/banking/seed_data.py @@ -5,7 +5,9 @@ db = SessionLocal() +# ========================= # 1. Create Customers +# ========================= customers = [ Customer( name="Amit Sharma", @@ -32,10 +34,13 @@ is_active=True ), ] + db.add_all(customers) db.commit() +# ========================= # 2. Create Accounts +# ========================= accounts = [ Account( account_number="AMIT12345", @@ -68,37 +73,13 @@ is_active=True ), ] -db.add_all(accounts) -db.commit() -# 3. Create Transactions (10 per customer, random dates in last 3 months) -merchant_list = [ - ("swiggy", "food"), ("zomato", "food"), ("amazon", "e-commerce"), - ("flipkart", "e-commerce"), ("electricity", "utility"), ("water", "utility"), - ("gas", "utility"), ("restaurant", "food"), ("myntra", "e-commerce"), ("mobile", "utility") -] - -transactions = [] -for i, account in enumerate(accounts): - for j in range(10): - merchant, category = random.choice(merchant_list) - txn_date = datetime.datetime(2024, 7, 6) + datetime.timedelta(days=random.randint(0, 92)) - txn = Transaction( - transaction_type="debit", - amount=random.randint(500, 3000), - recipient=merchant, - transaction_date=txn_date, - reference_id=f"TXN-{i+1}-{j+1}-{txn_date.strftime('%Y%m%d')}", - category=category, - payment_method="upi", - from_account_id=account.id - ) - transactions.append(txn) -db.add_all(transactions) +db.add_all(accounts) db.commit() -# 4. Create Beneficiaries (5 with duplicate name "Shailesh") -# 4. Create Beneficiaries (3 named "Shailesh" + 3 different names) +# ========================= +# 3. Create Beneficiaries +# ========================= beneficiaries = [ # 3 duplicates - Shailesh Beneficiary( @@ -167,5 +148,52 @@ db.add_all(beneficiaries) db.commit() -print("Seed data inserted successfully!") -db.close() \ No newline at end of file + +# ========================= +# 4. Create Transactions (Jan 2025 - 7th Oct 2025) +# ========================= +merchant_list = [ + ("swiggy", "food"), ("zomato", "food"), ("amazon", "e-commerce"), + ("flipkart", "e-commerce"), ("electricity", "utility"), ("water", "utility"), + ("gas", "utility"), ("restaurant", "food"), ("myntra", "e-commerce"), ("mobile", "utility") +] + +transactions_per_month = { + 1: 5, # Jan + 2: 3, # Feb + 3: 4, # Mar + 4: 6, # Apr + 5: 5, # May + 6: 3, # Jun + 7: 4, # Jul + 8: 2, # Aug + 9: 5, # Sep + 10: 7 # Oct (now 7 transactions, covering 1-7) +} + +transactions = [] + +for account in db.query(Account).all(): + for month, txn_count in transactions_per_month.items(): + for i in range(txn_count): + merchant, category = random.choice(merchant_list) + # For October, limit day to 1-7 + day = i + 1 if month == 10 else random.randint(1, 28) + txn_date = datetime.datetime(2025, month, day) + txn = Transaction( + transaction_type=random.choice(["debit", "credit"]), + amount=random.randint(500, 3000), + recipient=merchant, + transaction_date=txn_date, + reference_id=f"TXN-{account.id}-{month}-{i+1}-{txn_date.strftime('%Y%m%d')}", + category=category, + payment_method="upi", + from_account_id=account.id + ) + transactions.append(txn) + +db.add_all(transactions) +db.commit() + +print("Seed data inserted successfully (Jan 2025 - 7th Oct 2025)!") +db.close() From 4aa357a2eecd8191efa68d083ed4895bc8bca083 Mon Sep 17 00:00:00 2001 From: arpita-josh02 Date: Fri, 26 Sep 2025 12:32:39 +0530 Subject: [PATCH 10/17] added customer name in api --- service/banking/core_banking_routes.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/service/banking/core_banking_routes.py b/service/banking/core_banking_routes.py index 26d6f30..fa8dd2b 100644 --- a/service/banking/core_banking_routes.py +++ b/service/banking/core_banking_routes.py @@ -31,7 +31,9 @@ async def get_balance( detail=f"Customer with phone number '{phone}' not found " ) customer_id = customer.id - + else: + customer = db.query(Customer).filter(Customer.id == customer_id).first() + if not customer_id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -49,7 +51,7 @@ async def get_balance( detail=f"No active account found for customer ID {customer_id}" ) - return {"balance": account.balance,"customer_id":customer_id} + return {"balance": account.balance,"customer_id":customer_id,"customer_name": customer.name,} def resolve_conflict(to: str, matches, primary_field: str): From fa959d4693f164fd0e4fde2ab488a8fa315a7273 Mon Sep 17 00:00:00 2001 From: arpita-josh02 Date: Fri, 26 Sep 2025 16:01:31 +0530 Subject: [PATCH 11/17] added transcation in pay money and optimise code --- service/banking/core_banking_routes.py | 242 +++++++++++++------------ 1 file changed, 131 insertions(+), 111 deletions(-) diff --git a/service/banking/core_banking_routes.py b/service/banking/core_banking_routes.py index fa8dd2b..1e53123 100644 --- a/service/banking/core_banking_routes.py +++ b/service/banking/core_banking_routes.py @@ -5,7 +5,7 @@ from .database import get_db from .models import Customer, Account, Transaction, Beneficiary from pydantic import BaseModel -from typing import Optional +from typing import Optional, List router = APIRouter(prefix="/bank/me", tags=["banking"]) @@ -16,110 +16,63 @@ class PaymentRequest(BaseModel): payment_method: Optional[str] = None category: Optional[str] = None -@router.get("/balance") -async def get_balance( - customer_id: int = None, - phone: str = None, - db: Session = Depends(get_db) -): - """Get balance for a customer account (by customer_id or phone)""" - if phone: - customer = db.query(Customer).filter(Customer.phone == phone).first() - if not customer: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Customer with phone number '{phone}' not found " - ) - customer_id = customer.id - else: - customer = db.query(Customer).filter(Customer.id == customer_id).first() - - if not customer_id: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Missing required parameter: Please provide either 'customer_id' or 'phone'" - ) - - account = db.query(Account).filter( - Account.customer_id == customer_id, - Account.is_active == True - ).first() - - if not account: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"No active account found for customer ID {customer_id}" - ) - - return {"balance": account.balance,"customer_id":customer_id,"customer_name": customer.name,} - - -def resolve_conflict(to: str, matches, primary_field: str): - """ - Smart conflict resolution: - - If primary_field matches multiple entries: - - Check if nicknames differ -> show nickname differences - - Else check if tags differ -> show tag differences - - Else show generic message - """ - # Check nickname differences - nicknames = [b.nickname for b in matches if b.nickname] - unique_nicknames = set(nicknames) - if len(unique_nicknames) > 1: - details = [f"'{b.nickname}'" for b in matches if b.nickname] - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail=f"Multiple beneficiaries found with {primary_field} '{to}' with different nicknames: {', '.join(details)}. Please specify which nickname." - ) - - # Nicknames same or missing, check tags - tags = [b.tag for b in matches if b.tag] - unique_tags = set(tags) - if len(unique_tags) > 1: - details = [f"'{b.tag}'" for b in matches if b.tag] - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail=f"Multiple beneficiaries found with {primary_field} '{to}' with same nickname but different tags: {', '.join(details)}. Please specify which tag." - ) - - # No distinguishing nicknames or tags - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail=f"Multiple beneficiaries found with {primary_field} '{to}' with no distinguishing nickname or tag. Please use a more specific identifier." - ) - +def format_contact_details(contacts, limit=None): + """Helper function to format contact details for error messages""" + details = [] + for b in contacts: + identifier = f"'{b.name}'" + if b.nickname: + identifier += f" (nickname: {b.nickname})" + if b.tag: + identifier += f" (tag: {b.tag})" + details.append(identifier) + + if limit and len(details) > limit: + displayed = details[:limit] + more_count = len(details) - limit + return f"{', '.join(displayed)} and {more_count} more" + return ', '.join(details) def find_beneficiary(db: Session, customer_id: int, to: str): - """Find a beneficiary by name, nickname, or tag with conflict handling.""" + """Find a beneficiary by name, nickname, or tag with smart conflict handling.""" + # First try exact matches on each field for field in ["name", "nickname", "tag"]: matches = db.query(Beneficiary).filter( Beneficiary.customer_id == customer_id, getattr(Beneficiary, field).ilike(to) ).all() - + if matches: if len(matches) == 1: return matches[0] - - # Smart conflict resolution only for name - if field == "name": - resolve_conflict(to, matches, primary_field="name") - else: - # Fallback for nickname or tag - details = [] - for b in matches: - identifier = f"'{b.name}'" - if b.nickname: - identifier += f" (nickname: {b.nickname})" - if b.tag: - identifier += f" (tag: {b.tag})" - details.append(identifier) + + # Multiple matches found - check if we can distinguish by nickname or tag + nicknames = [b.nickname for b in matches if b.nickname] + unique_nicknames = set(nicknames) + if len(unique_nicknames) > 1: + nickname_options = ", ".join([f"'{nick}'" for nick in unique_nicknames if nick]) raise HTTPException( status_code=status.HTTP_409_CONFLICT, - detail=f"Multiple beneficiaries found with {field} '{to}': {', '.join(details)}. Please specify further." + detail=f"There are multiple '{to}' found in the beneficiaries. Please choose from: {', '.join(nicknames)}." ) + + # Try to distinguish by tags + tags = [b.tag for b in matches if b.tag] + unique_tags = set(tags) + if len(unique_tags) > 1: + tag_options = ", ".join([f"'{tag}'" for tag in unique_tags if tag]) + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=f"There are multiple '{to}' found in the beneficiaries with the same nickname but different tags. Please choose from: {', '.join(tags)}." + ) + + # Can't distinguish by either nickname or tag + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=f"There are multiple '{to}' found in the beneficiaries that can't be distinguished. Please use a nickname or tag to be more specific." + ) - # Partial match fallback + # No exact matches found, try partial matches matches = db.query(Beneficiary).filter( Beneficiary.customer_id == customer_id, ( @@ -132,8 +85,7 @@ def find_beneficiary(db: Session, customer_id: int, to: str): if not matches: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail=f"No beneficiary matching '{to}' found for this customer. " - f"Please check the name, nickname, or tag, or add as a new beneficiary." + detail=f"We couldn't find a beneficiary matching '{to}'. Please check the beneficiary name or add them as a new contact before sending money." ) if len(matches) > 1: @@ -147,11 +99,52 @@ def find_beneficiary(db: Session, customer_id: int, to: str): details.append(identifier) raise HTTPException( status_code=status.HTTP_409_CONFLICT, - detail=f"Multiple beneficiaries partially match '{to}': {', '.join(details)}. Please use a more specific name, nickname, or tag." + detail=f"There are multiple beneficiaries that partially match '{to}': {', '.join(details)}. Please use a more specific name, nickname, or tag." ) return matches[0] +@router.get("/balance") +async def get_balance( + customer_id: int = None, + phone: str = None, + db: Session = Depends(get_db) +): + """Get balance for a customer account (by customer_id or phone)""" + if phone: + customer = db.query(Customer).filter(Customer.phone == phone).first() + if not customer: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Invalid phone number '{phone}'. Please check the number and try again." + ) + customer_id = customer.id + else: + customer = db.query(Customer).filter(Customer.id == customer_id).first() + if not customer: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Invalid customer ID {customer_id}." + ) + + if not customer_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="To get your balance, please provide either a customer ID or a registered phone number." + ) + + account = db.query(Account).filter( + Account.customer_id == customer_id, + Account.is_active == True + ).first() + + if not account: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"No active account was found for customer ID {customer_id}. If you believe this is an error, please contact customer support." + ) + + return {"balance": account.balance,"customer_id":customer_id,"customer_name": customer.name} @router.post("/pay") async def pay_money( @@ -168,19 +161,19 @@ async def pay_money( if not customer: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail=f"Customer with phone number '{phone}' not found" + detail=f"We couldn't find a customer account associated with the phone number '{phone}'. Please check the number and try again." ) customer_id = customer.id if not customer_id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="Missing required parameter: Please provide either 'customer_id' or 'phone'" + detail="To make a payment, please provide either a customer ID or a registered phone number." ) to = request.to amount = request.amount - transaction_type = request.transaction_type + transaction_type = request.transaction_type or "debit" payment_method = request.payment_method or "upi" category = request.category @@ -214,15 +207,14 @@ async def pay_money( if not account: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail=f"No active account found for customer ID {customer_id}" + detail=f"No active account was found for customer ID {customer_id}. If you believe this is an error, please contact customer support." ) # Balance check if amount > account.balance: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Insufficient balance: Required ₹{amount:.2f}, " - f"available balance ₹{account.balance:.2f}" + detail=f"Your account balance of ₹{account.balance:.2f} is not enough to complete this transaction of ₹{amount:.2f}. Please add funds and try again." ) # Deduct balance @@ -238,10 +230,37 @@ async def pay_money( payment_method=payment_method, category=category, from_account_id=account.id, + transaction_date=datetime.now() # Explicitly set current time ) db.add(transaction) db.commit() + + # Refresh the transaction to get its ID and other database-generated values + db.refresh(transaction) + # Get 5 most recent transactions from the current date for this specific account + current_datetime = datetime.now() + + recent_transactions = db.query(Transaction).filter( + Transaction.from_account_id == account.id, # Filter by the current account ID + Transaction.transaction_date <= current_datetime # Filter by current datetime or before + ).order_by( + desc(Transaction.transaction_date) # Sort by transaction date descending + ).limit(5).all() + + # Format recent transactions for response + recent_txn_list = [] + for txn in recent_transactions: + recent_txn_list.append({ + "id": txn.id, + "amount": txn.amount, + "recipient": txn.recipient, + "transaction_date": txn.transaction_date.strftime("%Y-%m-%d %H:%M:%S"), + "reference_id": txn.reference_id, + "category": txn.category, + "payment_method": txn.payment_method, + "transaction_type": txn.transaction_type or "" + }) return { "status": "success", @@ -250,17 +269,16 @@ async def pay_money( "balance": account.balance, "reference_id": reference_id, "payment_method": payment_method, - "category": category + "category": category, + "recent_transactions": recent_txn_list } - - @router.get("/transactions") async def search_txn( customer_id: int = None, phone: str = None, recipient: str = None, category: str = None, - limit: int = None, + limit: int = 50, # Set a higher default limit start_date: str = None, end_date: str = None, db: Session = Depends(get_db) @@ -273,7 +291,7 @@ async def search_txn( if not customer: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail=f"Customer with phone number '{phone}' not found or is inactive" + detail=f"Invalid phone number '{phone}'." ) customer_id = customer.id @@ -282,8 +300,7 @@ async def search_txn( # Filter by customer_id if provided if customer_id is not None: accounts = db.query(Account.id).filter( - Account.customer_id == customer_id, - Account.is_active == True + Account.customer_id == customer_id ).all() account_ids = [a.id for a in accounts] if account_ids: @@ -307,7 +324,7 @@ async def search_txn( except ValueError: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Invalid start_date format '{start_date}'. Please use YYYY-MM-DD format (e.g., 2025-09-25)." + detail=f"The start date you entered ('{start_date}') is not in the correct format. Please use YYYY-MM-DD (e.g., 2025-09-25) and try again." ) if end_date: try: @@ -316,10 +333,13 @@ async def search_txn( except ValueError: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Invalid end_date format '{end_date}'. Please use YYYY-MM-DD format (e.g., 2025-09-25)." + detail=f"The end date you entered ('{end_date}') is not in the correct format. Please use YYYY-MM-DD (e.g., 2025-09-25) and try again." ) - # If dates provided, ignore limit - db_transactions = query.limit(limit if not (start_date or end_date) else None).all() + # Apply all filters first, then apply limit if provided + if limit: + db_transactions = query.limit(limit).all() + else: + db_transactions = query.all() return {"transactions": db_transactions} \ No newline at end of file From ced011dc3d58920316aaa5807b130ae5b5034d6a Mon Sep 17 00:00:00 2001 From: arpita-josh02 Date: Mon, 29 Sep 2025 12:51:28 +0530 Subject: [PATCH 12/17] correctd file name --- service/banking/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 service/banking/__init__.py diff --git a/service/banking/__init__.py b/service/banking/__init__.py new file mode 100644 index 0000000..7e908ea --- /dev/null +++ b/service/banking/__init__.py @@ -0,0 +1,5 @@ +# Initialize the banking package +from .database import Base, engine + +# Create tables when the package is imported +Base.metadata.create_all(bind=engine) \ No newline at end of file From 376d84d56e32717b4489e3e718ba310e676e4c91 Mon Sep 17 00:00:00 2001 From: arpita-josh02 Date: Tue, 30 Sep 2025 18:54:25 +0530 Subject: [PATCH 13/17] updated response and deleted wrong file --- service/banking/__init__.py: | 5 - service/banking/core_banking_routes.py | 248 +++++++++++++++++-------- 2 files changed, 175 insertions(+), 78 deletions(-) delete mode 100644 service/banking/__init__.py: diff --git a/service/banking/__init__.py: b/service/banking/__init__.py: deleted file mode 100644 index 7e908ea..0000000 --- a/service/banking/__init__.py: +++ /dev/null @@ -1,5 +0,0 @@ -# Initialize the banking package -from .database import Base, engine - -# Create tables when the package is imported -Base.metadata.create_all(bind=engine) \ No newline at end of file diff --git a/service/banking/core_banking_routes.py b/service/banking/core_banking_routes.py index 7887444..feb0ac8 100644 --- a/service/banking/core_banking_routes.py +++ b/service/banking/core_banking_routes.py @@ -6,6 +6,7 @@ from .models import Customer, Account, Transaction, Beneficiary from pydantic import BaseModel from typing import Optional, List +from fastapi.responses import JSONResponse router = APIRouter(prefix="/bank/me", tags=["banking"]) @@ -39,6 +40,7 @@ class PaymentRequest(BaseModel): transaction_type: Optional[str] = None payment_method: Optional[str] = None category: Optional[str] = None + otp: Optional[str] = None def format_contact_details(contacts, limit=None): """Helper function to format contact details for error messages""" @@ -66,6 +68,32 @@ def find_beneficiary(db: Session, customer_id: int, to: str): all_beneficiaries = db.query(Beneficiary).filter( Beneficiary.customer_id == customer_id ).all() + + # Helper function to format beneficiary list based on differences + def format_beneficiaries(matches): + same_name = len(set(b.name for b in matches)) == 1 + nicknames = [b.nickname for b in matches if b.nickname] + same_nickname = len(set(nicknames)) == 1 if nicknames else False + + result = [] + if same_name and nicknames and not same_nickname: + # Names are same, nicknames different - show nicknames + key_field = "nickname" + elif same_name and (not nicknames or same_nickname): + # Names same, no nicknames or same nicknames - show tags + key_field = "tag" + else: + # Different names - show actual names + key_field = "name" + + for b in matches: + result.append({ + "id": b.id, + "name": getattr(b, key_field) or "", + "account_number": b.account_number, + "status": "duplicate" + }) + return result # First, try exact matches on each field using normalized comparison for field in ["name", "nickname", "tag"]: @@ -79,59 +107,44 @@ def find_beneficiary(db: Session, customer_id: int, to: str): if len(matches) == 1: return matches[0] - # Multiple matches found - check if we can distinguish by nickname or tag - nicknames = [b.nickname for b in matches if b.nickname] - unique_nicknames = set(nicknames) - if len(unique_nicknames) > 1: - nickname_options = ", ".join([f"'{nick}'" for nick in unique_nicknames if nick]) - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail=f"There are multiple '{to}' exist in the beneficiaries. Please choose from: {', '.join(nicknames)}." - ) - - # Try to distinguish by tags - tags = [b.tag for b in matches if b.tag] - unique_tags = set(tags) - if len(unique_tags) > 1: - tag_options = ", ".join([f"'{tag}'" for tag in unique_tags if tag]) - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail=f"There are multiple '{to}' exist in the beneficiaries with the same nickname but different tags. Please choose from: {', '.join(tags)}." - ) - - # Can't distinguish by either nickname or tag - raise HTTPException( + # Multiple matches - return formatted list + beneficiary_list = format_beneficiaries(matches) + return JSONResponse( status_code=status.HTTP_409_CONFLICT, - detail=f"There are multiple '{to}' exist in the beneficiaries that can't be distinguished. Please use a nickname or tag to be more specific." + content={ + "status": "duplicate", + "message": f"Multiple beneficiaries found matching '{to}'", + "beneficiaries": beneficiary_list + } ) # No exact matches found, try partial matches using normalized comparison matches = [] for beneficiary in all_beneficiaries: - # Check if a normalized search term is contained in any normalized field - if (beneficiary.name and normalized_to in normalize_text(beneficiary.name)) or \ - (beneficiary.nickname and normalized_to in normalize_text(beneficiary.nickname)) or \ - (beneficiary.tag and normalized_to in normalize_text(beneficiary.tag)): + # Check if normalized search term is contained in any normalized field + if any( + getattr(beneficiary, field) and + normalized_to in normalize_text(getattr(beneficiary, field)) + for field in ["name", "nickname", "tag"] + ): matches.append(beneficiary) if not matches: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail=f"Beneficiary '{to}' does exist in your account. Please check the beneficiary name or add them as a new contact before sending money." + detail=f"Beneficiary '{to}' does not exist in your account. Please check the beneficiary name or add them as a new contact before sending money." ) if len(matches) > 1: - details = [] - for b in matches: - identifier = f"'{b.name}'" - if b.nickname: - identifier += f" (nickname: {b.nickname})" - if b.tag: - identifier += f" (tag: {b.tag})" - details.append(identifier) - raise HTTPException( + # Multiple partial matches - return formatted list + beneficiary_list = format_beneficiaries(matches) + return JSONResponse( status_code=status.HTTP_409_CONFLICT, - detail=f"There are multiple beneficiaries that partially match '{to}': {', '.join(details)}. Please use a more specific name, nickname, or tag." + content={ + "status": "duplicate", + "message": f"Multiple beneficiaries found matching '{to}'", + "beneficiaries": beneficiary_list + } ) return matches[0] @@ -208,27 +221,14 @@ async def pay_money( transaction_type = request.transaction_type or "debit" payment_method = request.payment_method or "upi" category = request.category + otp = request.otp # Resolve beneficiary beneficiary = find_beneficiary(db, customer_id, to) - - # Categorize merchant - food_merchants = ["swiggy", "zomato", "restaurant"] - ecommerce_merchants = ["amazon", "myntra", "flipkart"] - utility_merchants = ["electricity", "water", "gas", "mobile"] - - if category: - category_lower = category.lower() - if any(m in category_lower for m in food_merchants): - category = "food" - elif any(m in category_lower for m in ecommerce_merchants): - category = "e-commerce" - elif any(m in category_lower for m in utility_merchants): - category = "utility" - else: - category = "individual" - else: - category = "individual" + + # If we got a JSONResponse, return it directly + if isinstance(beneficiary, JSONResponse): + return beneficiary # Find active account account = db.query(Account).filter( @@ -248,6 +248,33 @@ async def pay_money( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Insufficient balance, your account balance of ₹{account.balance:.2f} is not enough to complete this transaction of ₹{amount:.2f}." ) + + # If OTP is not provided, return a confirmation message asking for OTP + if not otp: + + return { + "status": "otp", + "message": f"Please confirm that you want to pay ₹{amount:.2f} to {beneficiary.name}. Enter the OTP sent to your registered phone number to complete the transaction." + } + + + # Categorize merchant + food_merchants = ["swiggy", "zomato", "restaurant"] + ecommerce_merchants = ["amazon", "myntra", "flipkart"] + utility_merchants = ["electricity", "water", "gas", "mobile"] + + if category: + category_lower = category.lower() + if any(m in category_lower for m in food_merchants): + category = "food" + elif any(m in category_lower for m in ecommerce_merchants): + category = "e-commerce" + elif any(m in category_lower for m in utility_merchants): + category = "utility" + else: + category = "individual" + else: + category = "individual" # Deduct balance account.balance -= amount @@ -304,6 +331,8 @@ async def pay_money( "category": category, "recent_transactions": recent_txn_list } + + @router.get("/transactions") async def search_txn( customer_id: int = None, @@ -327,7 +356,8 @@ async def search_txn( ) customer_id = customer.id - query = db.query(Transaction).order_by(desc(Transaction.transaction_date)) + # Initialize the base query + base_query = db.query(Transaction).order_by(desc(Transaction.transaction_date)) # Filter by customer_id if provided if customer_id is not None: @@ -336,23 +366,52 @@ async def search_txn( ).all() account_ids = [a.id for a in accounts] if account_ids: - query = query.filter(Transaction.from_account_id.in_(account_ids)) + base_query = base_query.filter(Transaction.from_account_id.in_(account_ids)) else: return {"transactions": []} - - # Filter by recipient if provided + + # Get transaction IDs that match recipient filter + transaction_ids = set() + transactions_by_recipient = [] + transactions_by_category = [] + + # Apply recipient filter and get IDs if recipient: - query = query.filter(Transaction.recipient.ilike(f"%{recipient}%")) - - # Filter by category if provided + recipient_query = base_query.filter(Transaction.recipient.ilike(f"%{recipient}%")) + transactions_by_recipient = recipient_query.all() + if transactions_by_recipient: + transaction_ids = set(t.id for t in transactions_by_recipient) + + # Apply category filter and get IDs if category: - query = query.filter(Transaction.category.ilike(f"%{category}%")) - - # Filter by date range if provided + category_query = base_query.filter(Transaction.category.ilike(f"%{category}%")) + transactions_by_category = category_query.all() + if transactions_by_category: + category_ids = set(t.id for t in transactions_by_category) + + # If we already have recipient IDs, find the intersection + if transaction_ids: + transaction_ids = transaction_ids.intersection(category_ids) + else: + # If no recipient filter was applied, use category IDs + transaction_ids = category_ids + + # If neither recipient nor category was provided, use all transactions from base query + if not recipient and not category: + transaction_ids = set(t.id for t in base_query.all()) + + # Empty result if no transactions match the filters + if not transaction_ids: + return {"transactions": []} + + # Create a query for the filtered transaction IDs + filtered_query = db.query(Transaction).filter(Transaction.id.in_(transaction_ids)) + + # Apply date filters if start_date: try: start_dt = datetime.strptime(start_date, "%Y-%m-%d") - query = query.filter(Transaction.transaction_date >= start_dt) + filtered_query = filtered_query.filter(Transaction.transaction_date >= start_dt) except ValueError: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -361,17 +420,60 @@ async def search_txn( if end_date: try: end_dt = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1) - query = query.filter(Transaction.transaction_date < end_dt) + filtered_query = filtered_query.filter(Transaction.transaction_date < end_dt) except ValueError: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"The end date ('{end_date}') is not in the correct format. Please use YYYY-MM-DD (e.g., 2025-09-25) and try again." ) - - # Apply all filters first, then apply limit if provided + + # Apply sort order and limit + filtered_query = filtered_query.order_by(desc(Transaction.transaction_date)) + if limit: - db_transactions = query.limit(limit).all() + db_transactions = filtered_query.limit(limit).all() else: - db_transactions = query.all() + db_transactions = filtered_query.all() + + return {"transactions": db_transactions} + +@router.get("/beneficiaries") +def get_beneficiaries( + customer_id: int = None, + phone: str = None, + db: Session = Depends(get_db) +): + """Retrieve all beneficiaries for a given customer (by ID or phone) with all fields.""" + + if not customer_id and not phone: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Please provide either customer_id or phone." + ) + + # If phone is provided, find customer_id first + if phone: + customer = db.query(Customer).filter(Customer.phone == phone).first() + if not customer: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"No customer found with phone '{phone}'." + ) + customer_id = customer.id + + # Query beneficiaries + beneficiaries = db.query(Beneficiary).filter( + Beneficiary.customer_id == customer_id + ).all() + + if not beneficiaries: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="No beneficiaries found for this customer." + ) - return {"transactions": db_transactions} \ No newline at end of file + # Return all fields dynamically + return [ + {k: v for k, v in b.__dict__.items() if k != "_sa_instance_state"} + for b in beneficiaries + ] From 242634aae4e0442cfb49e36e93deaefbbce5a2b5 Mon Sep 17 00:00:00 2001 From: arpita-josh02 Date: Wed, 1 Oct 2025 15:29:05 +0530 Subject: [PATCH 14/17] corrected intent and seed data --- service/banking/seed_data.py | 413 ++++++++++++++++++++++++++++++----- service/detect_intent.py | 7 + service/orchestrator.py | 2 +- 3 files changed, 361 insertions(+), 61 deletions(-) diff --git a/service/banking/seed_data.py b/service/banking/seed_data.py index 069903f..5819cc1 100644 --- a/service/banking/seed_data.py +++ b/service/banking/seed_data.py @@ -1,10 +1,27 @@ -from database import SessionLocal -from models import Customer, Account, Transaction, Beneficiary +import os +import sys import datetime import random +import uuid +import string + +# Add the parent directory to sys.path +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +# For direct script execution +if __name__ == "__main__": + from database import SessionLocal + from models import Customer, Account, Transaction, Beneficiary +else: + # For module import + from banking.database import SessionLocal + from banking.models import Customer, Account, Transaction, Beneficiary db = SessionLocal() +# Get current date for data generation +current_date = datetime.datetime(2025, 10, 1) # Current date from context + # ========================= # 1. Create Customers # ========================= @@ -13,7 +30,7 @@ name="Amit Sharma", email="amit.sharma@example.com", phone="9876543210", - address="Mumbai, India", + address="B-404, Prateek Apartment, Andheri West, Mumbai, Maharashtra 400053, India", date_of_birth=datetime.datetime(1990, 5, 21), is_active=True ), @@ -21,7 +38,7 @@ name="Priya Singh", email="priya.singh@example.com", phone="9123456780", - address="Delhi, India", + address="C-12, Green Park Extension, New Delhi, Delhi 110016, India", date_of_birth=datetime.datetime(1988, 8, 15), is_active=True ), @@ -29,7 +46,7 @@ name="Rahul Verma", email="rahul.verma@example.com", phone="9988776655", - address="Bangalore, India", + address="506, Prestige Meridian, M.G. Road, Bangalore, Karnataka 560001, India", date_of_birth=datetime.datetime(1992, 2, 10), is_active=True ), @@ -41,34 +58,35 @@ # ========================= # 2. Create Accounts # ========================= +# Generate realistic account numbers and IFSC codes accounts = [ Account( - account_number="AMIT12345", + account_number="31245678901234", # SBI format (14 digits) account_type="savings", - balance=15000.0, + balance=145327.00, currency="INR", - branch="Mumbai Main", - ifsc_code="SBIN0000123", + branch="Mumbai Andheri", + ifsc_code="SBIN0001234", # SBI IFSC format (SBIN0XXXXX) customer_id=customers[0].id, is_active=True ), Account( - account_number="PRIYA54321", + account_number="5240780123456789", # HDFC format (16 digits) account_type="savings", - balance=22000.0, + balance=87425.50, currency="INR", - branch="Delhi Central", - ifsc_code="SBIN0000456", + branch="Delhi Green Park", + ifsc_code="HDFC0000456", # HDFC IFSC format customer_id=customers[1].id, is_active=True ), Account( - account_number="RAHUL67890", + account_number="09876543211234", # ICICI format (14 digits) account_type="current", - balance=30500.0, + balance=302753.25, currency="INR", - branch="Bangalore City", - ifsc_code="SBIN0000789", + branch="Bangalore MG Road", + ifsc_code="ICIC0000789", # ICICI IFSC format customer_id=customers[2].id, is_active=True ), @@ -83,40 +101,40 @@ beneficiaries = [ # 3 duplicates - Shailesh Beneficiary( - name="Shailesh", - account_number="SHL1001", + name="Shailesh Kumar", + account_number="33456789012345", # SBI format bank_name="SBI", customer_id=customers[0].id, - nickname="shailesh1", + nickname="sam", tag="friend", - ifsc_code="SBIN0000123", + ifsc_code="SBIN0004567", is_active=True ), Beneficiary( - name="Shailesh", - account_number="SHL1002", - bank_name="HDFC", + name="Shailesh Kumar", + account_number="5240780198765432", # HDFC format + bank_name="HDFC Bank", customer_id=customers[0].id, - nickname="shailesh2", + nickname="harsh", tag="colleague", - ifsc_code="HDFC0000456", + ifsc_code="HDFC0002345", is_active=True ), Beneficiary( - name="Shailesh", - account_number="SHL1003", - bank_name="ICICI", + name="Shailesh Kumar", + account_number="09876543219876", # ICICI format + bank_name="ICICI Bank", customer_id=customers[1].id, - nickname="shailesh3", + nickname="rajat", tag="family", - ifsc_code="ICIC0000789", + ifsc_code="ICIC0003456", is_active=True ), # 3 unique names Beneficiary( name="Ramesh Kumar", - account_number="RMK2001", + account_number="9123456789012", # Axis format bank_name="Axis Bank", customer_id=customers[1].id, nickname="ramesh", @@ -126,8 +144,8 @@ ), Beneficiary( name="Suresh Patil", - account_number="SRP2002", - bank_name="Kotak Mahindra", + account_number="1234567891234", # Kotak format + bank_name="Kotak Mahindra Bank", customer_id=customers[2].id, nickname="suresh", tag="business", @@ -136,7 +154,7 @@ ), Beneficiary( name="Anita Desai", - account_number="ANT2003", + account_number="1098765432123", # Yes Bank format bank_name="Yes Bank", customer_id=customers[0].id, nickname="anita", @@ -150,50 +168,325 @@ db.commit() # ========================= -# 4. Create Transactions (Jan 2025 - 7th Oct 2025) +# 4. Create Transactions with realistic data # ========================= merchant_list = [ - ("swiggy", "food"), ("zomato", "food"), ("amazon", "e-commerce"), - ("flipkart", "e-commerce"), ("electricity", "utility"), ("water", "utility"), - ("gas", "utility"), ("restaurant", "food"), ("myntra", "e-commerce"), ("mobile", "utility") + ("Swiggy", "food"), + ("Zomato", "food"), + ("Amazon", "e-commerce"), + ("Flipkart", "e-commerce"), + ("MSEB Mumbai", "utility"), + ("Delhi Jal Board", "utility"), + ("Mahanagar Gas", "utility"), + ("Taj Hotel", "dining"), + ("Myntra", "e-commerce"), + ("Jio Mobile", "utility"), + ("IRCTC", "travel"), + ("MakeMyTrip", "travel"), + ("Uber", "transportation"), + ("Ola", "transportation"), + ("Netflix", "entertainment"), + ("Hotstar", "entertainment"), + ("BigBasket", "groceries"), + ("Grofers", "groceries"), + ("Apollo Pharmacy", "healthcare"), + ("Medlife", "healthcare") +] + +# Additional food vendors for more variety +food_merchants = [ + ("Swiggy", "food"), + ("Zomato", "food"), + ("Domino's Pizza", "food"), + ("McDonald's", "food"), + ("KFC", "food"), + ("Pizza Hut", "food"), + ("Burger King", "food"), + ("Subway", "food"), + ("Wow! Momo", "food"), + ("Behrouz Biryani", "food"), + ("Faasos", "food"), + ("Sweet Truth", "food"), + ("Chaayos", "food"), + ("Starbucks", "food"), + ("Theobroma", "food") +] + +# E-commerce merchants +ecommerce_merchants = [ + ("Amazon", "e-commerce"), + ("Flipkart", "e-commerce"), + ("Myntra", "e-commerce"), + ("Ajio", "e-commerce"), + ("Tata CLiQ", "e-commerce"), + ("Nykaa", "e-commerce"), + ("Firstcry", "e-commerce"), + ("Snapdeal", "e-commerce"), + ("Meesho", "e-commerce"), + ("Reliance Digital", "e-commerce"), + ("Croma", "e-commerce"), + ("Pepperfry", "e-commerce") ] -transactions_per_month = { - 1: 5, # Jan - 2: 3, # Feb - 3: 4, # Mar - 4: 6, # Apr - 5: 5, # May - 6: 3, # Jun - 7: 4, # Jul - 8: 2, # Aug - 9: 5, # Sep - 10: 7 # Oct (now 7 transactions, covering 1-7) -} +payment_methods = ["upi", "neft", "rtgs", "imps", "card", "cash"] + +# Calculate date ranges +start_date = datetime.datetime(current_date.year, 1, 1) # Jan 1st of current year +months_to_generate = current_date.month # All months up to current month +days_in_current_month = current_date.day # Days in current month + +# Calculate last week's date range +one_week_ago = current_date - datetime.timedelta(days=7) +last_week_start = one_week_ago +last_week_end = current_date + +# Calculate last month's date range +one_month_ago = current_date.replace(day=1) - datetime.timedelta(days=1) +last_month_start = one_month_ago.replace(day=1) +last_month_end = current_date transactions = [] +# Generate regular transactions for all accounts for account in db.query(Account).all(): - for month, txn_count in transactions_per_month.items(): + # Generate transactions for each month + for month in range(1, months_to_generate + 1): + # Determine how many transactions to create this month + if month == current_date.month: + # For current month, only generate up to current day + txn_count = min(days_in_current_month, 15) # Max 15 txns per month but limit to current day + else: + # For past months, generate random number of transactions + txn_count = random.randint(5, 10) + + # Create transactions for this month for i in range(txn_count): merchant, category = random.choice(merchant_list) - # For October, limit day to 1-7 - day = i + 1 if month == 10 else random.randint(1, 28) - txn_date = datetime.datetime(2025, month, day) + + # For current month, limit day to current day + if month == current_date.month: + day = random.randint(1, days_in_current_month) + else: + # For other months, use any day in the month + day = random.randint(1, 28) + + txn_date = datetime.datetime(current_date.year, month, day) + + # Generate realistic transaction reference + payment_method = random.choice(payment_methods) + + # Generate reference ID based on payment method + if payment_method == "upi": + reference_id = f"UPI{uuid.uuid4().hex[:16].upper()}" + elif payment_method == "neft": + reference_id = f"NEFT{txn_date.strftime('%Y%m%d')}{random.randint(100000, 999999)}" + elif payment_method == "rtgs": + reference_id = f"RTGS{txn_date.strftime('%Y%m%d')}R{random.randint(10000, 99999)}" + elif payment_method == "imps": + reference_id = f"IMPS{random.randint(100000000, 999999999)}" + elif payment_method == "card": + reference_id = f"CARD{txn_date.strftime('%Y%m%d')}{random.randint(1000, 9999)}" + else: # cash + reference_id = f"CASH{txn_date.strftime('%Y%m%d')}{random.randint(1000, 9999)}" + + # Determine realistic amount based on category + if category == "utility": + amount = random.randint(500, 3000) + elif category == "food": + amount = random.randint(200, 1500) + elif category == "e-commerce": + amount = random.randint(1000, 10000) + elif category == "travel": + amount = random.randint(2000, 15000) + elif category == "transportation": + amount = random.randint(100, 500) + elif category == "entertainment": + amount = random.randint(200, 1000) + elif category == "groceries": + amount = random.randint(500, 5000) + elif category == "healthcare": + amount = random.randint(500, 3000) + elif category == "dining": + amount = random.randint(1000, 8000) + else: + amount = random.randint(500, 5000) + + # Most transactions are debits, but some are credits + transaction_type = "debit" if random.random() < 0.8 else "credit" + + # Create transaction object txn = Transaction( - transaction_type=random.choice(["debit", "credit"]), - amount=random.randint(500, 3000), + transaction_type=transaction_type, + amount=amount, recipient=merchant, transaction_date=txn_date, - reference_id=f"TXN-{account.id}-{month}-{i+1}-{txn_date.strftime('%Y%m%d')}", + reference_id=reference_id, category=category, - payment_method="upi", + payment_method=payment_method, from_account_id=account.id ) transactions.append(txn) +# ========================= +# 5. Add special transactions for Customer 1 and 2 +# ========================= + +# For first two customer accounts +for account_idx in range(2): # Only for customer 1 and 2 + account = accounts[account_idx] + + # Add multiple Swiggy transactions in the last month + for i in range(8): + # Spread across the last month + days_back = random.randint(0, 30) + txn_date = current_date - datetime.timedelta(days=days_back) + + # Random food merchant, but higher probability of Swiggy + merchant, category = random.choice(food_merchants) if random.random() < 0.7 else ("Swiggy", "food") + + # Realistic food amount + amount = random.randint(200, 800) + + # Mostly UPI payments for food + payment_method = "upi" if random.random() < 0.8 else random.choice(["card", "cash"]) + + reference_id = f"UPI{uuid.uuid4().hex[:16].upper()}" if payment_method == "upi" else f"CARD{txn_date.strftime('%Y%m%d')}{random.randint(1000, 9999)}" + + txn = Transaction( + transaction_type="debit", + amount=amount, + recipient=merchant, + transaction_date=txn_date, + reference_id=reference_id, + category="food", + payment_method=payment_method, + from_account_id=account.id + ) + transactions.append(txn) + + # Add more frequent Swiggy transactions in the last week + for i in range(5): + # Spread across the last week + days_back = random.randint(0, 6) + txn_date = current_date - datetime.timedelta(days=days_back) + + # Higher probability of Swiggy in the last week + merchant = "Swiggy" if random.random() < 0.6 else random.choice(["Zomato", "Domino's Pizza", "McDonald's"]) + + amount = random.randint(200, 600) # Typical food order amounts + + payment_method = "upi" + reference_id = f"UPI{uuid.uuid4().hex[:16].upper()}" + + txn = Transaction( + transaction_type="debit", + amount=amount, + recipient=merchant, + transaction_date=txn_date, + reference_id=reference_id, + category="food", + payment_method=payment_method, + from_account_id=account.id + ) + transactions.append(txn) + + # Add multiple Amazon transactions spread over the last month + for i in range(6): + days_back = random.randint(0, 30) + txn_date = current_date - datetime.timedelta(days=days_back) + + # Random e-commerce merchant, but higher probability of Amazon + merchant = "Amazon" if random.random() < 0.7 else random.choice([m[0] for m in ecommerce_merchants]) + + # Realistic Amazon purchase amounts + amount = random.randint(500, 5000) + + # Card is more common for e-commerce + payment_method = "card" if random.random() < 0.7 else "upi" + + if payment_method == "card": + reference_id = f"CARD{txn_date.strftime('%Y%m%d')}{random.randint(1000, 9999)}" + else: + reference_id = f"UPI{uuid.uuid4().hex[:16].upper()}" + + txn = Transaction( + transaction_type="debit", + amount=amount, + recipient=merchant, + transaction_date=txn_date, + reference_id=reference_id, + category="e-commerce", + payment_method=payment_method, + from_account_id=account.id + ) + transactions.append(txn) + + # Add concentrated Amazon purchases in the last week + for i in range(3): + days_back = random.randint(0, 6) + txn_date = current_date - datetime.timedelta(days=days_back) + + # Last week mostly Amazon + merchant = "Amazon" + + # Varying purchase amounts + amount = random.randint(1000, 8000) + + payment_method = "card" + reference_id = f"CARD{txn_date.strftime('%Y%m%d')}{random.randint(1000, 9999)}" + + txn = Transaction( + transaction_type="debit", + amount=amount, + recipient=merchant, + transaction_date=txn_date, + reference_id=reference_id, + category="e-commerce", + payment_method=payment_method, + from_account_id=account.id + ) + transactions.append(txn) + + # Add specific queries for "last week transactions of food, Swiggy" + # Make sure we have transactions for every day of the last week + for day in range(7): + txn_date = current_date - datetime.timedelta(days=day) + + # Higher probability of Swiggy + if random.random() < 0.7: + merchant = "Swiggy" + else: + merchant = random.choice(["Zomato", "Domino's Pizza", "KFC"]) + + amount = random.randint(250, 750) # Realistic food delivery amounts + + payment_method = "upi" + reference_id = f"UPI{uuid.uuid4().hex[:16].upper()}" + + txn = Transaction( + transaction_type="debit", + amount=amount, + recipient=merchant, + transaction_date=txn_date, + reference_id=reference_id, + category="food", + payment_method=payment_method, + from_account_id=account.id + ) + transactions.append(txn) + db.add_all(transactions) db.commit() -print("Seed data inserted successfully (Jan 2025 - 7th Oct 2025)!") +# Calculate some stats for the print message +total_transactions = len(transactions) +food_transactions = sum(1 for t in transactions if t.category == 'food') +ecommerce_transactions = sum(1 for t in transactions if t.category == 'e-commerce') +swiggy_transactions = sum(1 for t in transactions if t.recipient == 'Swiggy') +amazon_transactions = sum(1 for t in transactions if t.recipient == 'Amazon') + +print(f"Seed data inserted successfully (Jan {current_date.year} - {current_date.day} {current_date.strftime('%b')} {current_date.year})!") +print(f"Total transactions: {total_transactions}") +print(f"Food transactions: {food_transactions} (Swiggy: {swiggy_transactions})") +print(f"E-commerce transactions: {ecommerce_transactions} (Amazon: {amazon_transactions})") db.close() \ No newline at end of file diff --git a/service/detect_intent.py b/service/detect_intent.py index 0c252d3..a2fd132 100644 --- a/service/detect_intent.py +++ b/service/detect_intent.py @@ -77,6 +77,9 @@ User: "How much I spend amazon last week" {"intent":"txn_insights","entities":{"timeframe":"last_week","recipient":"amazon", "category":"shopping"},"language":"{lang}"} +User: "How much I spend on food" +{"intent":"txn_insights","entities":{"category":"food"},"language":"{lang}"} + “Show me my last 5 Swiggy transactions” {"intent":"txn_insights","entities":{"count":5,"recipient":"swiggy"},"language":"{lang}"} @@ -187,6 +190,7 @@ def translate(message:str, lang_code: str = "en"): def detect_intent_with_llama(transcript: str, lang_hint: str = "en") -> Dict[str, Any]: #transcript = "how much i spend on amazon last month?" + # transcript="How much I spend on food" try: response = ollama.Client(host=ollama_host).generate( system = SYSTEM, @@ -297,3 +301,6 @@ def determine_action(intent: str, entities: dict) -> str: return "respond" else: return "unknown" + +# print(detect_intent_with_llama("","en")) + diff --git a/service/orchestrator.py b/service/orchestrator.py index 6548d2c..9d0f92e 100644 --- a/service/orchestrator.py +++ b/service/orchestrator.py @@ -653,7 +653,7 @@ async def _fetch_transactions_with_filters(self, entities: Dict[str, Any], custo params["end_date"] = end_date # Only add a limit if no date filtering is used - if not (start_date or end_date): + if not (start_date or end_date) and count is not None: params["limit"] = count # Make API call From 6745e4570c6bbc1affc27fb26689ec2fdace1a8d Mon Sep 17 00:00:00 2001 From: arpita-josh02 Date: Thu, 2 Oct 2025 19:15:25 +0530 Subject: [PATCH 15/17] updated transcation response --- service/banking/seed_data.py | 4 ++-- service/orchestrator.py | 30 ++++++++++++------------------ 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/service/banking/seed_data.py b/service/banking/seed_data.py index 5819cc1..60fdbec 100644 --- a/service/banking/seed_data.py +++ b/service/banking/seed_data.py @@ -111,11 +111,11 @@ is_active=True ), Beneficiary( - name="Shailesh Kumar", + name="Shailesh Gupta", account_number="5240780198765432", # HDFC format bank_name="HDFC Bank", customer_id=customers[0].id, - nickname="harsh", + nickname="kumar", tag="colleague", ifsc_code="HDFC0002345", is_active=True diff --git a/service/orchestrator.py b/service/orchestrator.py index 3733f21..4fe6f7d 100644 --- a/service/orchestrator.py +++ b/service/orchestrator.py @@ -144,7 +144,7 @@ def _calculate_category_insights(transactions: list, category: str, period_desc: def _calculate_general_insights(transactions: list, period_desc: str) -> Dict[str, Any]: - """Calculate general spending insights.""" + """Calculate general spending insights with focus on top category and merchant.""" total_spent = sum(abs(t.get("amount", 0)) for t in transactions if IS_DEBIT(t)) if not transactions: @@ -153,41 +153,35 @@ def _calculate_general_insights(transactions: list, period_desc: str) -> Dict[st "message": f"No spending data found for {period_desc}." } - # Find a top-spending recipient + # Find spending by recipient recipient_totals = {} for t in transactions: if IS_DEBIT(t): # Only debits recipient_name = t.get("recipient", "Unknown") recipient_totals[recipient_name] = recipient_totals.get(recipient_name, 0) + abs(t.get("amount", 0)) - # Find the top-spending category + # Find spending by category category_totals = {} for t in transactions: if IS_DEBIT(t): # Only debits category_name = t.get("category", "Unknown") category_totals[category_name] = category_totals.get(category_name, 0) + abs(t.get("amount", 0)) - - # Build a message with both recipient and category insights - message_parts = [] - if recipient_totals: - top_recipient = max(recipient_totals.items(), key=lambda x: x[1]) - message_parts.append(f"Your top spending recipient {period_desc} was {top_recipient[0]} at {top_recipient[1]:,.2f} INR") - if category_totals: + message = f"No spending data found for {period_desc}." + + if category_totals and recipient_totals: + # Get top category and amount top_category = max(category_totals.items(), key=lambda x: x[1]) - message_parts.append(f"Your top spending category {period_desc} was {top_category[0]} at {top_category[1]:,.2f} INR") + + # Get top recipient overall + top_recipient = max(recipient_totals.items(), key=lambda x: x[1]) + + message = f"{period_desc}, you highest spent ₹{top_category[1]:,.2f} on {top_category[0]}, especially ₹{top_recipient[1]:,.2f} on {top_recipient[0]}" - if message_parts: - message = ". ".join(message_parts) + f". Total spent: {total_spent:,.2f} INR." - else: - message = f"You've spent {total_spent:,.2f} INR {period_desc}." - return { "total_spent": total_spent, "message": message } - - def _calculate_spending_insights(transactions: list, entities: Dict[str, Any]) -> Dict[str, Any]: """Calculate spending insights based on transactions and filter criteria.""" recipient = entities.get("recipient") From d2675f9453fd3088b029f27592fd5e9949d7da51 Mon Sep 17 00:00:00 2001 From: arpita-josh02 Date: Fri, 17 Oct 2025 12:21:17 +0530 Subject: [PATCH 16/17] add v2 api for bank --- service/banking/core_banking_routes_v2.py | 445 ++++++++++++++++++++++ 1 file changed, 445 insertions(+) create mode 100644 service/banking/core_banking_routes_v2.py diff --git a/service/banking/core_banking_routes_v2.py b/service/banking/core_banking_routes_v2.py new file mode 100644 index 0000000..bb41e74 --- /dev/null +++ b/service/banking/core_banking_routes_v2.py @@ -0,0 +1,445 @@ +from fastapi import APIRouter, Depends, HTTPException, Response, status +from sqlalchemy.orm import Session +from sqlalchemy import desc +from datetime import datetime, timedelta +from .database import get_db +from .models import Customer, Account, Transaction, Beneficiary +from pydantic import BaseModel +from typing import Optional, List +from fastapi.responses import JSONResponse + +router = APIRouter(prefix="/bank/me/v2", tags=["banking"]) + +def normalize_text(text: str) -> str: + """Normalize text for beneficiary matching by converting to lowercase, removing spaces, + and replacing textual numbers with digits.""" + if not text: + return "" + + # Convert to lowercase + normalized = text.lower() + + # Remove spaces + normalized = normalized.replace(" ", "") + + # Replace textual numbers with digits + number_replacements = { + "zero": "0", "one": "1", "two": "2", "three": "3", "four": "4", + "five": "5", "six": "6", "seven": "7", "eight": "8", "nine": "9", + "ten": "10" + } + + for word, digit in number_replacements.items(): + normalized = normalized.replace(word, digit) + + return normalized + +class PaymentRequest(BaseModel): + to: str + amount: float + transaction_type: Optional[str] = None + payment_method: Optional[str] = None + category: Optional[str] = None + otp: Optional[str] = None + +def format_contact_details(contacts, limit=None): + """Helper function to format contact details for error messages""" + details = [] + for b in contacts: + identifier = f"'{b.name}'" + if b.nickname: + identifier += f" (nickname: {b.nickname})" + if b.tag: + identifier += f" (tag: {b.tag})" + details.append(identifier) + + if limit and len(details) > limit: + displayed = details[:limit] + more_count = len(details) - limit + return f"{', '.join(displayed)} and {more_count} more" + return ', '.join(details) + +def find_beneficiary(db: Session, customer_id: int, to: str): + """Find a beneficiary by name, nickname, or tag with smart conflict handling.""" + # Normalize the search query + normalized_to = normalize_text(to) + + # Get all beneficiaries for this customer + all_beneficiaries = db.query(Beneficiary).filter( + Beneficiary.customer_id == customer_id + ).all() + + # Helper function to format beneficiary list based on differences + def format_beneficiaries(matches): + same_name = len(set(b.name for b in matches)) == 1 + nicknames = [b.nickname for b in matches if b.nickname] + same_nickname = len(set(nicknames)) == 1 if nicknames else False + + result = [] + if same_name and nicknames and not same_nickname: + # Names are same, nicknames different - show nicknames + key_field = "nickname" + elif same_name and (not nicknames or same_nickname): + # Names same, no nicknames or same nicknames - show tags + key_field = "tag" + else: + # Different names - show actual names + key_field = "name" + + for b in matches: + result.append({ + "id": b.id, + "name": getattr(b, key_field) or "", + "account_number": b.account_number, + }) + return result + + # First, try exact matches on each field using normalized comparison + for field in ["name", "nickname", "tag"]: + matches = [] + for beneficiary in all_beneficiaries: + field_value = getattr(beneficiary, field) + if field_value and normalized_to in normalize_text(field_value): + matches.append(beneficiary) + + if matches: + if len(matches) == 1: + return matches[0] + + # Multiple matches - return formatted list + beneficiary_list = format_beneficiaries(matches) + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail={ + "status": "duplicate", + "message": f"Multiple beneficiaries found matching '{to}'. Please confirm the correct beneficiary", + "beneficiaries": beneficiary_list + } + ) + + # No exact matches found, try partial matches using normalized comparison + matches = [] + for beneficiary in all_beneficiaries: + # Check if normalized search term is contained in any normalized field + if any( + getattr(beneficiary, field) and + normalized_to in normalize_text(getattr(beneficiary, field)) + for field in ["name", "nickname", "tag"] + ): + matches.append(beneficiary) + + if not matches: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Beneficiary '{to}' does not exist in your account. Please check the beneficiary name or add them as a new contact before sending money." + ) + + if len(matches) > 1: + # Multiple partial matches - return formatted list + beneficiary_list = format_beneficiaries(matches) + return HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail={ + "status": "duplicate", + "message": f"Multiple beneficiaries found matching '{to}'. Please confirm the correct beneficiary", + "beneficiaries": beneficiary_list + } + ) + + return matches[0] + +@router.get("/balance") +async def get_balance( + customer_id: int = None, + phone: str = None, + db: Session = Depends(get_db) +): + """Get balance for a customer account (by customer_id or phone)""" + if phone: + customer = db.query(Customer).filter(Customer.phone == phone).first() + if not customer: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Invalid phone number '{phone}'. Please check the number and try again." + ) + customer_id = customer.id + else: + customer = db.query(Customer).filter(Customer.id == customer_id).first() + if not customer: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Invalid customer ID {customer_id}." + ) + + if not customer_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="To know your balance, please provide either a customer ID or a registered phone number." + ) + + account = db.query(Account).filter( + Account.customer_id == customer_id, + Account.is_active == True + ).first() + + if not account: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"No active account was found for customer ID {customer_id}. If you believe this is an error, please contact customer support." + ) + + data = { + "balance": account.balance, + "customer_id": customer_id, + "customer_name": customer.name + } + + return { + "status": "success", + "message": f"Balance retrieved successfully for {customer.name}.", + "data": data + } + +@router.post("/pay") +async def pay_money( + request: PaymentRequest, + customer_id: int = None, + phone: str = None, + db: Session = Depends(get_db) +): + """Send money to a merchant or contact""" + if phone: + customer = db.query(Customer).filter(Customer.phone == phone).first() + if not customer: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Incorrect Phone number '{phone}'. Please check the number and try again." + ) + customer_id = customer.id + + if not customer_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="To transfer money, please provide either a customer ID or a registered phone number." + ) + + to = request.to + amount = request.amount + transaction_type = request.transaction_type or "debit" + payment_method = request.payment_method or "upi" + category = request.category + otp = request.otp + + beneficiary = find_beneficiary(db, customer_id, to) + account = db.query(Account).filter( + Account.customer_id == customer_id, + Account.is_active == True + ).first() + + if not account: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Customer ID {customer_id} is not valid. If you believe this is an error, please contact customer support." + ) + + if amount > account.balance: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Insufficient balance, your account balance of ₹{account.balance:.2f} is not enough to complete this transaction of ₹{amount:.2f}." + ) + + if not otp: + return { + "status": "otp", + "message": f"Please confirm the transaction ₹{amount:.2f} to {beneficiary.name} by entering the OTP sent to your registered mobile number.", + "data": {} + } + + # Category mapping + food_merchants = ["swiggy", "zomato", "restaurant"] + ecommerce_merchants = ["amazon", "myntra", "flipkart"] + utility_merchants = ["electricity", "water", "gas", "mobile"] + + if category: + category_lower = category.lower() + if any(m in category_lower for m in food_merchants): + category = "food" + elif any(m in category_lower for m in ecommerce_merchants): + category = "e-commerce" + elif any(m in category_lower for m in utility_merchants): + category = "utility" + else: + category = "individual" + else: + category = "individual" + + account.balance -= amount + + reference_id = f"TXN-{datetime.now().strftime('%Y%m%d%H%M%S')}" + transaction = Transaction( + transaction_type=transaction_type, + amount=amount, + recipient=beneficiary.name, + reference_id=reference_id, + payment_method=payment_method, + category=category, + from_account_id=account.id, + transaction_date=datetime.now() + ) + + db.add(transaction) + db.commit() + db.refresh(transaction) + + current_datetime = datetime.now() + recent_transactions = db.query(Transaction).filter( + Transaction.from_account_id == account.id, + Transaction.transaction_date <= current_datetime + ).order_by(desc(Transaction.transaction_date)).limit(5).all() + + recent_txn_list = [{ + "id": txn.id, + "amount": txn.amount, + "recipient": txn.recipient, + "transaction_date": txn.transaction_date.strftime("%Y-%m-%d %H:%M:%S"), + "reference_id": txn.reference_id, + "category": txn.category, + "payment_method": txn.payment_method, + "transaction_type": txn.transaction_type or "" + } for txn in recent_transactions] + + data = { + "to": beneficiary.name, + "amount": amount, + "balance": account.balance, + "reference_id": reference_id, + "payment_method": payment_method, + "category": category, + "recent_transactions": recent_txn_list + } + + return { + "status": "success", + "message": f"₹{amount:.2f} sent successfully to {beneficiary.name}.", + "data": data + } + +@router.get("/transactions") +async def search_txn( + customer_id: int = None, + phone: str = None, + recipient: str = None, + category: str = None, + limit: int = 50, + start_date: str = None, + end_date: str = None, + db: Session = Depends(get_db) +): + if phone: + customer = db.query(Customer).filter( + Customer.phone == phone, + Customer.is_active == True + ).first() + if not customer: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Invalid phone number '{phone}'." + ) + customer_id = customer.id + + base_query = db.query(Transaction).order_by(desc(Transaction.transaction_date)) + + if customer_id is not None: + accounts = db.query(Account.id).filter(Account.customer_id == customer_id).all() + account_ids = [a.id for a in accounts] + if account_ids: + base_query = base_query.filter(Transaction.from_account_id.in_(account_ids)) + else: + return {"status": "success", "message": "No transactions found.", "data": {"transactions": []}} + + transaction_ids = set() + if recipient: + recipient_query = base_query.filter(Transaction.recipient.ilike(f"%{recipient}%")) + transactions_by_recipient = recipient_query.all() + transaction_ids = set(t.id for t in transactions_by_recipient) + + if category: + category_query = base_query.filter(Transaction.category.ilike(f"%{category}%")) + transactions_by_category = category_query.all() + category_ids = set(t.id for t in transactions_by_category) + transaction_ids = transaction_ids.intersection(category_ids) if transaction_ids else category_ids + + if not recipient and not category: + transaction_ids = set(t.id for t in base_query.all()) + + if not transaction_ids: + return {"status": "success", "message": "No matching transactions found.", "data": {"transactions": []}} + + filtered_query = db.query(Transaction).filter(Transaction.id.in_(transaction_ids)) + + if start_date: + try: + start_dt = datetime.strptime(start_date, "%Y-%m-%d") + filtered_query = filtered_query.filter(Transaction.transaction_date >= start_dt) + except ValueError: + raise HTTPException(status_code=400, detail=f"Invalid start date '{start_date}', use YYYY-MM-DD format.") + + if end_date: + try: + end_dt = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1) + filtered_query = filtered_query.filter(Transaction.transaction_date < end_dt) + except ValueError: + raise HTTPException(status_code=400, detail=f"Invalid end date '{end_date}', use YYYY-MM-DD format.") + + db_transactions = filtered_query.order_by(desc(Transaction.transaction_date)).limit(limit).all() + + data = {"transactions": db_transactions} + + return { + "status": "success", + "message": f"{len(db_transactions)} transaction(s) retrieved successfully.", + "data": data + } + +@router.get("/beneficiaries") +def get_beneficiaries( + customer_id: int = None, + phone: str = None, + db: Session = Depends(get_db) +): + """Retrieve all beneficiaries for a given customer (by ID or phone) with all fields.""" + + if not customer_id and not phone: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Please provide either customer_id or phone." + ) + + # If phone is provided, find customer_id first + if phone: + customer = db.query(Customer).filter(Customer.phone == phone).first() + if not customer: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"No customer found with phone '{phone}'." + ) + customer_id = customer.id + + # Query beneficiaries + beneficiaries = db.query(Beneficiary).filter( + Beneficiary.customer_id == customer_id + ).all() + + if not beneficiaries: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="No beneficiaries found for this customer." + ) + + # Return all fields dynamically + beneficiaries = [ + {k: v for k, v in b.__dict__.items() if k != "_sa_instance_state"} + for b in beneficiaries + ] + + return{"beneficiaries": beneficiaries} From 435c4469c789a3516506c6b777a5eb0702f20b68 Mon Sep 17 00:00:00 2001 From: arpita-josh02 Date: Thu, 23 Oct 2025 18:07:25 +0530 Subject: [PATCH 17/17] chages releted to connect langflow and code --- service/banking/core_banking_routes_v2.py | 2 +- service/config.py | 33 ++++ service/main.py | 178 +++++++++++++++++++++- 3 files changed, 209 insertions(+), 4 deletions(-) diff --git a/service/banking/core_banking_routes_v2.py b/service/banking/core_banking_routes_v2.py index bb41e74..bf11107 100644 --- a/service/banking/core_banking_routes_v2.py +++ b/service/banking/core_banking_routes_v2.py @@ -222,7 +222,7 @@ async def pay_money( status_code=status.HTTP_400_BAD_REQUEST, detail="To transfer money, please provide either a customer ID or a registered phone number." ) - + print(request) to = request.to amount = request.amount transaction_type = request.transaction_type or "debit" diff --git a/service/config.py b/service/config.py index 0e6dcda..352b1a2 100644 --- a/service/config.py +++ b/service/config.py @@ -24,3 +24,36 @@ redis_port = int(os.getenv("REDIS_PORT", 6379)) redis_db = int(os.getenv("REDIS_DB", 0)) redis_password = os.getenv("REDIS_PASSWORD", None) +import os +from dotenv import load_dotenv + +load_dotenv() + +openai_api_key = os.getenv("OPENAI_API_KEY") +#model_id = os.getenv('MODEL_ID', 'large-v3') +model_id = os.getenv('MODEL_ID','small') +model_path = os.getenv('MODEL_PATH', './models') +ollama_host = os.getenv("OLLAMA_HOST", "http://ollama:11434") +ollama_model_name = os.getenv("OLLAMA_MODEL_NAME", "llama3.2") +open_ai_model_name = os.getenv("OPENAI_MODEL_NAME", "gpt-4") +ollama_translation_model_name = os.getenv("OLLAMA_TRANS_MODEL","gemma2:latest") +open_ai_temperature = os.getenv("OPENAI_TEMPERATURE", 0.2) +db_user = os.getenv("DB_USER") +db_password = os.getenv("DB_PASSWORD") +db_host = os.getenv("DB_HOST") +db_port = os.getenv("DB_PORT") +db_name = os.getenv("DB_NAME") + +sarvam_api_key = os.getenv("SARVAM_API_KEY","sk_t7fvsjjb_7JsD5ZXGrEhHqjUtAQSFsCxB") +# Redis configuration +redis_host = os.getenv("REDIS_HOST", "localhost") +redis_port = int(os.getenv("REDIS_PORT", 6379)) +redis_db = int(os.getenv("REDIS_DB", 0)) +redis_password = os.getenv("REDIS_PASSWORD", None) + +# Langflow API configuration +langflow_api_url = os.getenv("LANGFLOW_API_URL", "http://localhost:7860") +langflow_flow_id = os.getenv("LANGFLOW_FLOW_ID", "df6ef421-30ef-4901-bc8b-270c2ce61d41") +langflow_api_key = os.getenv("LANGFLOW_API_KEY", "sk-SCQyDlsYB7qPzmzL3yivQs-J5JmvX82uHVbDiGWrQR8") +langflow_timeout = int(os.getenv("LANGFLOW_TIMEOUT", 60)) # timeout in seconds + diff --git a/service/main.py b/service/main.py index f11c046..3f84f4f 100644 --- a/service/main.py +++ b/service/main.py @@ -1,11 +1,13 @@ -from fastapi import FastAPI, UploadFile, File, Form, Depends +from fastapi import FastAPI, UploadFile, File, Form, Depends, BackgroundTasks from fastapi.responses import JSONResponse from logger import logger from dotenv import load_dotenv +from config import langflow_api_url, langflow_flow_id, langflow_api_key, langflow_timeout from starlette.middleware.cors import CORSMiddleware from audio_service import translate_with_whisper from audio_service import translate_with_whisper_timestamped, translate_with_whisper_from_upload from detect_intent import detect_intent_with_llama, format_intent_response, translate +from typing import Dict, Any, List, Optional from summarizer import summarize_using_openai from summarizer import summarize_using_ollama from pydantic import BaseModel @@ -13,15 +15,23 @@ from util import generate_timestamp_json from fastapi_versionizer.versionizer import Versionizer, api_version import json -from banking.core_banking_routes import router as banking_router +from banking.core_banking_routes_v2 import router as banking_router from orchestrator import orchestrate_banking_request from typing import Optional import httpx from redis_client import session_manager from datetime import datetime from session_service import SessionService, SessionFlowProcessor +from contextlib import asynccontextmanager -app = FastAPI() +# Set up async HTTP client for Langflow API +@asynccontextmanager +async def lifespan(app: FastAPI): + app.state.langflow_client = httpx.AsyncClient() + yield + await app.state.langflow_client.aclose() + +app = FastAPI(lifespan=lifespan) # Add CORS middleware to the application app.add_middleware( @@ -232,3 +242,165 @@ async def transcribe_intent( logger.error(f"Error in transcribe-intent: {traceback.format_exc()}") current_session_id = session_id if session_id else "unknown" return JSONResponse(content={"message": str(e), "session_id": current_session_id}, status_code=500) + + +@app.post("/voice/process-with-langflow") +async def process_with_langflow( + audio: Optional[UploadFile] = File(None), + session_id: Optional[str] = Form(None), + customer_id: Optional[int] = Form(None), + phone: Optional[str] = Form(None), + api_key: Optional[str] = Form(None) +): + """ + Process audio through Langflow for intent detection and API execution. + + Steps: + 1. Audio is transcribed using Whisper + 2. Transcribed text is sent to Langflow for processing + 3. Langflow processes the intent and calls necessary APIs + 4. Results are returned to the client + """ + try: + if not audio: + return JSONResponse(status_code=400, content={"message": "No audio file provided"}) + + # Step 1: Transcribe audio + id, response, lang, dia = translate_with_whisper_from_upload(audio) + translation_text = response[1] + language = lang[1] + + logger.info("Translation done") + logger.info(f"Translated text: {translation_text}") + logger.info(f"Detected language: {language}") + + # # Verify request has required authentication + # server_api_key = langflow_api_key + # if server_api_key: + # return JSONResponse( + # status_code=401, + # content={"message": "Invalid API key. Please provide a valid API key to access this endpoint."} + # ) + + # Step 2: Send to Langflow + langflow_response = await call_langflow_api(translation_text, language, customer_id, phone, session_id, api_key) + + # Step 3: Format and return response + return JSONResponse(content={ + "status": "success", + "message": "Audio processed through Langflow successfully", + "translation": translation_text, + "language": language, + "response": langflow_response, + "session_id": session_id + }, status_code=200) + + except Exception as e: + logger.error(f"Error in process-with-langflow: {traceback.format_exc()}") + current_session_id = session_id if session_id else "unknown" + + # Categorize errors for better client handling + status_code = 500 + error_type = "server_error" + + if "Invalid API key" in str(e): + status_code = 401 + error_type = "authentication_error" + elif "timed out" in str(e): + status_code = 504 + error_type = "timeout_error" + elif "rate limit" in str(e): + status_code = 429 + error_type = "rate_limit_error" + + return JSONResponse(content={ + "message": str(e), + "session_id": current_session_id, + "error_type": error_type + }, status_code=status_code) + + +async def call_langflow_api(text: str, language: str, customer_id: Optional[int] = None, + phone: Optional[str] = None, session_id: Optional[str] = None, + client_api_key: Optional[str] = None) -> Dict[str, Any]: + """ + Call the Langflow API with the translated text. + + Args: + text: The translated text + language: Detected language code + customer_id: Optional customer ID + phone: Optional phone number + session_id: Optional session ID + + Returns: + Processed response from Langflow + """ + try: + # Construct the URL for the Langflow API + url = f"{langflow_api_url}/api/v1/run/{langflow_flow_id}" + + # Prepare the payload + payload = { + "inputs": { + "text": text, + "language": language, + "customer_id": customer_id, + "phone": phone, + "session_id": session_id + }, + "tweaks": {} + } + + # Set up headers + headers = { + "Content-Type": "application/json" + } + + # Add API key if available - prioritize client API key over server API key + api_key_to_use = client_api_key or langflow_api_key + if api_key_to_use: + # Set the x-api-key header as required by Langflow v1.5+ + headers["x-api-key"] = api_key_to_use + + logger.info(f"Calling Langflow API at {url}") + # Make the API call + async with httpx.AsyncClient(timeout=langflow_timeout) as client: + response = await client.post(url, json=payload, headers=headers) + + # Check if the request was successful + response.raise_for_status() + + # Additional validation for empty responses + if response.status_code == 204 or not response.text: + logger.warning("Langflow API returned an empty response") + return {"warning": "Langflow returned an empty response. The flow might not be configured correctly."} + + # Handle rate limiting + if response.status_code == 429: + logger.warning("Langflow API rate limit reached") + raise Exception("Langflow API rate limit reached. Please try again later.") + + # Parse the response + result = response.json() + logger.info(f"Langflow API response: {result}") + # Sanitize the result to avoid exposing sensitive information + if isinstance(result, dict): + # Remove any potential sensitive information + result.pop("api_key", None) + result.pop("auth_token", None) + result.pop("password", None) + + logger.info(f"Langflow API response processed successfully") + + return result + except httpx.TimeoutException: + logger.error("Langflow API call timed out") + raise Exception("Langflow processing timed out. Please try again later.") + except httpx.HTTPStatusError as e: + logger.error(f"Langflow API HTTP error: {e}") + raise Exception(f"Error calling Langflow API: {e.response.status_code} - {e.response.text}") + except Exception as e: + logger.error(f"Error calling Langflow API: {str(e)}") + raise Exception(f"Error processing with Langflow: {str(e)}") +