diff --git a/BRANCH_STATS_API_DOCUMENTATION.md b/BRANCH_STATS_API_DOCUMENTATION.md new file mode 100644 index 0000000..4aec0ba --- /dev/null +++ b/BRANCH_STATS_API_DOCUMENTATION.md @@ -0,0 +1,132 @@ +# Branch Statistics API Documentation + +## Overview +This API provides comprehensive statistics for branch accounts including joint accounts, fixed deposits, and savings accounts. + +## Endpoints + +### 1. Get All Branches List +**GET** `/api/branches/list` + +Returns a list of all branches for dropdown selection. + +**Response:** +```json +{ + "branches": [ + { + "branch_id": "3dd6870c-e6f2-414d-9973-309ba00ce115", + "branch_name": "Main Branch", + "branch_code": "BR001", + "city": "Colombo" + }, + { + "branch_id": "b5c3a0d2-1234-5678-9abc-def012345678", + "branch_name": "Mount Lavinia Branch", + "branch_code": "BR002", + "city": "Mount Lavinia" + } + ], + "total_count": 2 +} +``` + +### 2. Get Branch Account Statistics +**GET** `/api/branches/{branch_id}/statistics` + +Returns comprehensive statistics for a specific branch. + +**Parameters:** +- `branch_id` (path parameter): UUID of the branch + +**Response:** +```json +{ + "branch_id": "3dd6870c-e6f2-414d-9973-309ba00ce115", + "branch_name": "Main Branch", + "branch_code": "BR001", + "total_joint_accounts": 5, + "joint_accounts_balance": 250000.00, + "total_fixed_deposits": 10, + "fixed_deposits_amount": 1500000.00, + "total_savings_accounts": 25, + "savings_accounts_balance": 750000.00 +} +``` + +## Statistics Details + +### Joint Accounts +- **total_joint_accounts**: Count of accounts with more than one owner +- **joint_accounts_balance**: Combined balance of all joint accounts + +### Fixed Deposits +- **total_fixed_deposits**: Count of fixed deposit accounts in the branch +- **fixed_deposits_amount**: Total amount invested in fixed deposits + +### Savings/Current Accounts +- **total_savings_accounts**: Count of regular accounts (excluding joint accounts to avoid double counting) +- **savings_accounts_balance**: Combined balance of all savings accounts + +## Authentication +Both endpoints require authentication. Include the JWT token in the Authorization header: +``` +Authorization: Bearer +``` + +## Usage Example + +### Frontend Integration (React/TypeScript) + +```typescript +// 1. Fetch branches for dropdown +const fetchBranches = async () => { + const response = await fetch('http://localhost:8000/api/branches/list', { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + const data = await response.json(); + return data.branches; +}; + +// 2. Fetch statistics for selected branch +const fetchBranchStats = async (branchId: string) => { + const response = await fetch( + `http://localhost:8000/api/branches/${branchId}/statistics`, + { + headers: { + 'Authorization': `Bearer ${token}` + } + } + ); + return await response.json(); +}; +``` + +## Files Created + +### Backend Structure +``` +app/ +├── schemas/ +│ └── branch_stats_schema.py # Pydantic models +├── repositories/ +│ └── branch_stats_repo.py # Database queries +├── services/ +│ └── branch_stats_service.py # Business logic +└── api/ + └── branch_stats_routes.py # API endpoints +``` + +### Database Query Logic +The repository uses PostgreSQL CTEs (Common Table Expressions) to: +1. Calculate joint accounts (accounts with multiple owners) +2. Calculate fixed deposits linked to the branch +3. Calculate savings accounts (excluding joint accounts) +4. Combine all statistics in a single query + +## Error Handling +- **404**: Branch not found +- **500**: Internal server error with detailed message +- **401**: Unauthorized (missing or invalid token) diff --git a/app/api/branch_stats_routes.py b/app/api/branch_stats_routes.py new file mode 100644 index 0000000..6d0e57a --- /dev/null +++ b/app/api/branch_stats_routes.py @@ -0,0 +1,68 @@ +from fastapi import APIRouter, Depends, HTTPException, Request +from app.database.db import get_db +from app.repositories.branch_stats_repo import BranchStatsRepository +from app.services.branch_stats_service import BranchStatsService +from app.schemas.branch_stats_schema import BranchAccountStats, BranchListResponse + +router = APIRouter() + +def get_branch_stats_service(db=Depends(get_db)) -> BranchStatsService: + """Dependency to get BranchStatsService instance""" + branch_stats_repo = BranchStatsRepository(db) + return BranchStatsService(branch_stats_repo) + +def get_current_user(request: Request): + """Simple dependency to get current authenticated user from request state""" + if not hasattr(request.state, 'user') or not request.state.user: + raise HTTPException(status_code=401, detail="Authentication required") + return request.state.user + +@router.get("/branches/list", response_model=BranchListResponse) +def get_all_branches( + current_user: dict = Depends(get_current_user), + branch_stats_service: BranchStatsService = Depends(get_branch_stats_service) +): + """ + Get list of all branches for dropdown selection + + Returns: + - List of branches with branch_id, branch_name, branch_code, city + - Total count of branches + + This endpoint is useful for populating dropdown menus + """ + return branch_stats_service.get_all_branches_list() + +@router.get("/branches/{branch_id}/statistics", response_model=BranchAccountStats) +def get_branch_account_statistics( + branch_id: str, + current_user: dict = Depends(get_current_user), + branch_stats_service: BranchStatsService = Depends(get_branch_stats_service) +): + """ + Get comprehensive account statistics for a specific branch + + Returns statistics for: + - **Joint Accounts**: Total count and combined balance + - **Fixed Deposits**: Total count and combined amount + - **Savings/Current Accounts**: Total count and combined balance (excluding joint accounts) + + Parameters: + - **branch_id**: UUID of the branch + + Example Response: + ```json + { + "branch_id": "3dd6870c-e6f2-414d-9973-309ba00ce115", + "branch_name": "Main Branch", + "branch_code": "BR001", + "total_joint_accounts": 5, + "joint_accounts_balance": 250000.00, + "total_fixed_deposits": 10, + "fixed_deposits_amount": 1500000.00, + "total_savings_accounts": 25, + "savings_accounts_balance": 750000.00 + } + ``` + """ + return branch_stats_service.get_branch_statistics(branch_id) diff --git a/app/main.py b/app/main.py index b413d29..6f31e7c 100644 --- a/app/main.py +++ b/app/main.py @@ -1,4 +1,4 @@ -from app.api import joint_account_management_routes, pdf_report_routes +from app.api import joint_account_management_routes, pdf_report_routes, branch_stats_routes from fastapi import FastAPI, Request from app.api import customer_branch_routes, savings_plan_routes, transaction_management_routes, user_routes @@ -65,6 +65,10 @@ app.include_router(pdf_report_routes.router, prefix='/api/pdf-reports', tags=["PDF Reports"]) +# Branch statistics routes +app.include_router(branch_stats_routes.router, + prefix='/api', tags=["Branch Statistics"]) + @app.get("/") async def root(): diff --git a/app/repositories/branch_stats_repo.py b/app/repositories/branch_stats_repo.py new file mode 100644 index 0000000..d22d651 --- /dev/null +++ b/app/repositories/branch_stats_repo.py @@ -0,0 +1,130 @@ +from typing import Dict, List, Any, Optional +from psycopg2.extensions import connection +from psycopg2.extras import RealDictCursor + +class BranchStatsRepository: + def __init__(self, conn: connection): + self.conn = conn + self.cursor = conn.cursor(cursor_factory=RealDictCursor) + + def get_branch_account_statistics(self, branch_id: str) -> Dict[str, Any]: + """ + Get comprehensive account statistics for a specific branch + including joint accounts, fixed deposits, and savings accounts + """ + try: + self.cursor.execute( + """ + WITH branch_info AS ( + SELECT + branch_id, + name as branch_name, + address as branch_address + FROM branch + WHERE branch_id = %s::UUID + ), + joint_accounts_stats AS ( + SELECT + a.acc_id, + a.balance + FROM account a + INNER JOIN accounts_owner ao ON a.acc_id = ao.acc_id + WHERE a.branch_id = %s::UUID + GROUP BY a.acc_id, a.balance + HAVING COUNT(ao.customer_id) > 1 + ), + joint_accounts_summary AS ( + SELECT + COUNT(*) as total_joint_accounts, + COALESCE(SUM(balance), 0) as joint_accounts_balance + FROM joint_accounts_stats + ), + fixed_deposits_stats AS ( + SELECT + COUNT(*) as total_fixed_deposits, + COALESCE(SUM(fd.balance), 0) as fixed_deposits_amount + FROM fixed_deposit fd + INNER JOIN account a ON fd.acc_id = a.acc_id + WHERE a.branch_id = %s::UUID + ), + savings_accounts_stats AS ( + SELECT + COUNT(DISTINCT a.acc_id) as total_savings_accounts, + COALESCE(SUM(a.balance), 0) as savings_accounts_balance + FROM account a + WHERE a.branch_id = %s::UUID + AND a.acc_id NOT IN ( + -- Exclude joint accounts + SELECT acc_id + FROM accounts_owner + GROUP BY acc_id + HAVING COUNT(customer_id) > 1 + ) + ) + SELECT + bi.branch_id, + bi.branch_name, + bi.branch_address, + COALESCE(jas.total_joint_accounts, 0) as total_joint_accounts, + COALESCE(jas.joint_accounts_balance, 0) as joint_accounts_balance, + COALESCE(fds.total_fixed_deposits, 0) as total_fixed_deposits, + COALESCE(fds.fixed_deposits_amount, 0) as fixed_deposits_amount, + COALESCE(sas.total_savings_accounts, 0) as total_savings_accounts, + COALESCE(sas.savings_accounts_balance, 0) as savings_accounts_balance + FROM branch_info bi + LEFT JOIN joint_accounts_summary jas ON 1=1 + LEFT JOIN fixed_deposits_stats fds ON 1=1 + LEFT JOIN savings_accounts_stats sas ON 1=1 + """, + (branch_id, branch_id, branch_id, branch_id) + ) + + result = self.cursor.fetchone() + + if result: + return { + 'branch_id': str(result['branch_id']), + 'branch_name': result['branch_name'], + 'branch_address': result.get('branch_address'), + 'total_joint_accounts': int(result['total_joint_accounts']), + 'joint_accounts_balance': float(result['joint_accounts_balance']), + 'total_fixed_deposits': int(result['total_fixed_deposits']), + 'fixed_deposits_amount': float(result['fixed_deposits_amount']), + 'total_savings_accounts': int(result['total_savings_accounts']), + 'savings_accounts_balance': float(result['savings_accounts_balance']) + } + + return None + + except Exception as e: + raise e + + def get_all_branches(self) -> List[Dict[str, Any]]: + """ + Get list of all branches for dropdown selection + """ + try: + self.cursor.execute( + """ + SELECT + branch_id, + name as branch_name, + address as branch_address + FROM branch + ORDER BY name + """ + ) + + branches = self.cursor.fetchall() + + return [ + { + 'branch_id': str(branch['branch_id']), + 'branch_name': branch['branch_name'], + 'branch_address': branch.get('branch_address') + } + for branch in branches + ] + + except Exception as e: + raise e diff --git a/app/schemas/branch_stats_schema.py b/app/schemas/branch_stats_schema.py new file mode 100644 index 0000000..bb050d6 --- /dev/null +++ b/app/schemas/branch_stats_schema.py @@ -0,0 +1,33 @@ +from pydantic import BaseModel, Field +from typing import Optional, List + +class BranchAccountStats(BaseModel): + """Statistics for different account types in a branch""" + + # Joint Accounts + total_joint_accounts: int = Field(..., description="Total number of joint accounts") + joint_accounts_balance: float = Field(..., description="Total balance in joint accounts") + + # Fixed Deposits + total_fixed_deposits: int = Field(..., description="Total number of fixed deposit accounts") + fixed_deposits_amount: float = Field(..., description="Total amount in fixed deposits") + + # Savings/Current Accounts + total_savings_accounts: int = Field(..., description="Total number of savings/current accounts") + savings_accounts_balance: float = Field(..., description="Total balance in savings accounts") + + # Branch Info + branch_id: str + branch_name: str + branch_address: Optional[str] = None + +class BranchListItem(BaseModel): + """Branch information for dropdown""" + branch_id: str + branch_name: str + branch_address: Optional[str] = None + +class BranchListResponse(BaseModel): + """List of all branches for dropdown""" + branches: List[BranchListItem] + total_count: int diff --git a/app/services/branch_stats_service.py b/app/services/branch_stats_service.py new file mode 100644 index 0000000..86f8c7d --- /dev/null +++ b/app/services/branch_stats_service.py @@ -0,0 +1,71 @@ +from fastapi import HTTPException +from app.repositories.branch_stats_repo import BranchStatsRepository +from app.schemas.branch_stats_schema import BranchAccountStats, BranchListResponse, BranchListItem + +class BranchStatsService: + def __init__(self, branch_stats_repo: BranchStatsRepository): + self.branch_stats_repo = branch_stats_repo + + def get_branch_statistics(self, branch_id: str) -> BranchAccountStats: + """ + Get comprehensive statistics for a branch including: + - Joint accounts count and balance + - Fixed deposits count and amount + - Savings accounts count and balance + """ + try: + stats = self.branch_stats_repo.get_branch_account_statistics(branch_id) + + if not stats: + raise HTTPException( + status_code=404, + detail=f"Branch with ID {branch_id} not found" + ) + + return BranchAccountStats( + branch_id=stats['branch_id'], + branch_name=stats['branch_name'], + branch_code=stats.get('branch_code'), + total_joint_accounts=stats['total_joint_accounts'], + joint_accounts_balance=stats['joint_accounts_balance'], + total_fixed_deposits=stats['total_fixed_deposits'], + fixed_deposits_amount=stats['fixed_deposits_amount'], + total_savings_accounts=stats['total_savings_accounts'], + savings_accounts_balance=stats['savings_accounts_balance'] + ) + + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"Error fetching branch statistics: {str(e)}" + ) + + def get_all_branches_list(self) -> BranchListResponse: + """ + Get list of all branches for dropdown selection + """ + try: + branches_data = self.branch_stats_repo.get_all_branches() + + branches = [ + BranchListItem( + branch_id=branch['branch_id'], + branch_name=branch['branch_name'], + branch_code=branch.get('branch_code'), + city=branch.get('city') + ) + for branch in branches_data + ] + + return BranchListResponse( + branches=branches, + total_count=len(branches) + ) + + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"Error fetching branches list: {str(e)}" + ) diff --git a/requirements.txt b/requirements.txt index 545fe03..421cd15 100644 Binary files a/requirements.txt and b/requirements.txt differ