Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions BRANCH_STATS_API_DOCUMENTATION.md
Original file line number Diff line number Diff line change
@@ -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 <your-token>
```

## 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)
68 changes: 68 additions & 0 deletions app/api/branch_stats_routes.py
Original file line number Diff line number Diff line change
@@ -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)
6 changes: 5 additions & 1 deletion app/main.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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():
Expand Down
130 changes: 130 additions & 0 deletions app/repositories/branch_stats_repo.py
Original file line number Diff line number Diff line change
@@ -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:
Copy link

Copilot AI Oct 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-raising exceptions without adding context or logging defeats the purpose of the try-catch block. Either add logging/context or remove the exception handling entirely to let exceptions bubble up naturally.

Copilot uses AI. Check for mistakes.
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
Comment on lines +106 to +130
Copy link

Copilot AI Oct 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-raising exceptions without adding context or logging defeats the purpose of the try-catch block. Either add logging/context or remove the exception handling entirely to let exceptions bubble up naturally.

Suggested change
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
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
]

Copilot uses AI. Check for mistakes.
33 changes: 33 additions & 0 deletions app/schemas/branch_stats_schema.py
Original file line number Diff line number Diff line change
@@ -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
Loading