from fastapi import FastAPI, Depends, HTTPException from sqlalchemy.orm import Session from .database import get_db from .models import User, TransactionAccount, Transaction, TransactionBreakdown, Tag, TagAssignedToTransaction from pydantic import BaseModel from typing import List, Optional from datetime import datetime from fastapi.security import OAuth2PasswordBearer from .auth import create_access_token, decode_access_token, is_admin, hash_password, verify_password from sqlalchemy import func, literal_column, select # Initialize FastAPI app app = FastAPI() oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login/") # Pydantic schemas for validation and response models class UserCreate(BaseModel): user_name: str email: str password: str class UserResponse(BaseModel): user_id: int user_name: str email: str class Config: from_attributes = True class TransactionAccountCreate(BaseModel): account_name: str balance: float class TransactionAccountResponse(BaseModel): transaction_account_id: int account_name: str balance: float class Config: from_attributes = True class TransactionBreakdownResponse(BaseModel): transaction_account_id: int earned_amount: float spent_amount: float class Config: from_attributes = True class TransactionCreateRequest(BaseModel): transaction_name: str amount: float = 0.0 # Default to 0 if not provided date: Optional[datetime] = None # Default to None, will use current time if not provided tag_id: Optional[int] = None # Optional tag target_account_id: int # Mandatory target account breakdowns: Optional[List[TransactionBreakdownResponse]] = None # Optional list of breakdowns class TransactionUpdate(BaseModel): transaction_name: str = None amount: float = None net_amount: float = None date: datetime = None class TransactionResponse(BaseModel): transaction_id: int transaction_name: str amount: float net_amount: float date: datetime class Config: json_encoders = { datetime: lambda v: v.isoformat() # Serialize datetime as ISO 8601 string } from_attributes = True class TagCreate(BaseModel): tag_name: str class TagResponse(BaseModel): tag_id: int tag_name: str class Config: from_attributes = True class TagAssign(BaseModel): transaction_id: int tag_id: int class AuthRequest(BaseModel): user_name: Optional[str] = None email: str password: str class AuthResponse(BaseModel): access_token: str token_type: str # Dependency to get the current user def get_current_user( token: str = Depends(oauth2_scheme), db: Session = Depends(get_db) ): """ Retrieves the current user based on the access token. """ try: payload = decode_access_token(token) user_id = payload.get("sub") if not user_id: raise HTTPException( status_code=401, detail="Invalid authentication credentials", headers={"WWW-Authenticate": "Bearer"} ) user = db.query(User).filter(User.user_id == user_id).first() if not user: raise HTTPException( status_code=401, detail="Invalid authentication credentials", headers={"WWW-Authenticate": "Bearer"} ) return user except Exception as e: print(f"Error decoding token or fetching user: {e}") raise HTTPException( status_code=401, detail="Invalid authentication credentials", headers={"WWW-Authenticate": "Bearer"} ) # Routes @app.get("/") def read_root(): return {"message": "Welcome to the Fein Prototype API"} @app.post("/auth/register/", response_model=AuthResponse) def register( auth_request: AuthRequest, db: Session = Depends(get_db) ): # Check if email already exists existing_user = db.query(User).filter(User.email == auth_request.email).first() if existing_user: raise HTTPException( status_code=400, detail="Email already registered" ) # Hash password and create new user hashed_password = hash_password(auth_request.password) new_user = User( user_name=auth_request.user_name, email=auth_request.email, password=hashed_password ) db.add(new_user) db.commit() db.refresh(new_user) # Return access token access_token = create_access_token({"sub": new_user.user_id, "email": new_user.email}) return {"access_token": access_token, "token_type": "bearer"} @app.post("/auth/login/", response_model=AuthResponse) def login( auth_request: AuthRequest, db: Session = Depends(get_db) ): # Verify email and password user = db.query(User).filter(User.email == auth_request.email).first() if not user or not verify_password(auth_request.password, user.password): raise HTTPException( status_code=401, detail="Invalid email or password" ) # Return access token access_token = create_access_token({"sub": user.user_id, "email": user.email}) return {"access_token": access_token, "token_type": "bearer"} @app.get("/admin/accounts/", response_model=List[TransactionAccountResponse]) def admin_get_all_accounts( user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """ Admin can fetch all transaction accounts. """ if not is_admin(user.email): raise HTTPException( status_code=403, detail="Access denied" ) return db.query(TransactionAccount).all() @app.post("/accounts/", response_model=TransactionAccountResponse) def create_account( account: TransactionAccountCreate, user: User = Depends(get_current_user), db: Session = Depends(get_db) ): new_account = TransactionAccount( account_name=account.account_name, balance=account.balance, user_id=user.user_id ) db.add(new_account) db.commit() db.refresh(new_account) return new_account @app.get("/accounts/", response_model=List[TransactionAccountResponse]) def get_accounts( user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """ Admin can fetch all accounts, regular users only their accounts. """ query = db.query(TransactionAccount) if is_admin(user.email): return query.all() return query.filter(TransactionAccount.user_id == user.user_id).all() @app.post("/transactions/", response_model=TransactionResponse) def create_transaction( transaction_request: TransactionCreateRequest, user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """ Admins can create transactions for any account; regular users only for their accounts. Create a transaction and associate it with the user's accounts via breakdowns. """ # Admin bypasses ownership checks if not is_admin(user.email): # Validate target account ownership target_account = db.query(TransactionAccount).filter( TransactionAccount.transaction_account_id == transaction_request.target_account_id, TransactionAccount.user_id == user.user_id ).first() if not target_account: raise HTTPException( status_code=403, detail="Access denied to target account." ) # Create transaction new_transaction = Transaction( transaction_name=transaction_request.transaction_name, amount=transaction_request.amount, net_amount=0.0, # Will be updated based on breakdowns date=transaction_request.date or datetime.utcnow(), # Use current UTC time if not provided ) db.add(new_transaction) db.commit() db.refresh(new_transaction) # Associate a tag, if provided if transaction_request.tag_id: tag = db.query(Tag).filter(Tag.tag_id == transaction_request.tag_id).first() if tag: tag_assignment = TagAssignedToTransaction( transaction_id=new_transaction.transaction_id, tag_id=tag.tag_id ) db.add(tag_assignment) # Add breakdowns net_amount = 0.0 if transaction_request.breakdowns: for breakdown in transaction_request.breakdowns: # Validate breakdown account ownership breakdown_account = db.query(TransactionAccount).filter( TransactionAccount.transaction_account_id == breakdown.transaction_account_id, TransactionAccount.user_id == user.user_id ).first() if not breakdown_account: raise HTTPException( status_code=403, detail=f"Access denied to breakdown account {breakdown.transaction_account_id}." ) # Create breakdown new_breakdown = TransactionBreakdown( transaction_id=new_transaction.transaction_id, transaction_account_id=breakdown.transaction_account_id, earned_amount=breakdown.earned_amount, spent_amount=breakdown.spent_amount ) db.add(new_breakdown) # Calculate net amount net_amount += breakdown.earned_amount - breakdown.spent_amount # Update transaction's net amount new_transaction.net_amount = net_amount db.commit() db.refresh(new_transaction) return new_transaction @app.get("/transactions/", response_model=List[TransactionResponse]) def get_transactions( user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """ Fetch transactions based on user role, excluding placeholder transactions. - Admin: Fetch all non-placeholder transactions. - Regular User: Fetch non-placeholder transactions tied to the user's accounts via transaction breakdowns. """ query = db.query(Transaction) if is_admin(user.email): # Admins see all non-placeholder transactions transactions = ( query .filter(~Transaction.transaction_name.like("Tag_%_placeholder")) .all() ) else: transactions = ( query .join(TransactionBreakdown, Transaction.transaction_id == TransactionBreakdown.transaction_id) .join(TransactionAccount, TransactionBreakdown.transaction_account_id == TransactionAccount.transaction_account_id) .filter(TransactionAccount.user_id == user.user_id) .filter(~Transaction.transaction_name.like("Tag_%_placeholder")) # Exclude placeholders .all() ) return [ { **transaction.__dict__, "date": transaction.date.isoformat() # Convert datetime to ISO 8601 string } for transaction in transactions ] @app.get("/transactions/{transaction_id}", response_model=TransactionResponse) def get_transaction_by_id( transaction_id: int, user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """ Retrieve a single transaction by its ID, ensuring access is restricted to the transaction creator or an admin user. """ # If the user is an admin, they can access any transaction if is_admin(user.email): transaction = db.query(Transaction).filter(Transaction.transaction_id == transaction_id).first() if not transaction: raise HTTPException( status_code=404, detail="Transaction not found." ) return transaction # Otherwise, restrict access to the transaction creator transaction = ( db.query(Transaction) .join(TransactionBreakdown, Transaction.transaction_id == TransactionBreakdown.transaction_id) .join(TransactionAccount, TransactionBreakdown.transaction_account_id == TransactionAccount.transaction_account_id) .filter(Transaction.transaction_id == transaction_id) .filter(TransactionAccount.user_id == user.user_id) .first() ) if not transaction: raise HTTPException( status_code=404, detail="Transaction not found or access denied." ) return transaction @app.get("/transactions/{transaction_id}/breakdowns", response_model=List[TransactionBreakdownResponse]) def get_transaction_breakdowns( transaction_id: int, user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """ Fetch transaction breakdowns for a specific transaction. """ breakdowns = ( db.query(TransactionBreakdown) .join(TransactionAccount, TransactionBreakdown.transaction_account_id == TransactionAccount.transaction_account_id) .filter(TransactionBreakdown.transaction_id == transaction_id) .filter(TransactionAccount.user_id == user.user_id) .all() ) if not breakdowns: raise HTTPException( status_code=404, detail="No breakdowns found for this transaction." ) return breakdowns @app.put("/transactions/{transaction_id}", response_model=TransactionResponse) def update_transaction( transaction_id: int, transaction_update: TransactionUpdate, user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """ Admins can update any transaction Regular users update a transaction only if it belongs to the logged-in user. """ query = db.query(Transaction) if is_admin(user.email): transaction = ( query .filter(Transaction.transaction_id == transaction_id) .first() ) if not transaction: raise HTTPException( status_code=404, detail="Transaction not found." ) else: transaction = ( query .join(TransactionBreakdown, Transaction.transaction_id == TransactionBreakdown.transaction_id) .join(TransactionAccount, TransactionBreakdown.transaction_account_id == TransactionAccount.transaction_account_id) .filter(Transaction.transaction_id == transaction_id) .filter(TransactionAccount.user_id == user.user_id) .first() ) if not transaction: raise HTTPException( status_code=404, detail="Transaction not found or access denied." ) # Update transaction fields for key, value in transaction_update.dict(exclude_unset=True).items(): setattr(transaction, key, value) db.commit() db.refresh(transaction) return transaction @app.delete("/transactions/{transaction_id}") def delete_transaction( transaction_id: int, user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """ Admins can delete any transaction Regular users can delete a transaction only if it belongs to the logged-in user. """ query = db.query(Transaction) if is_admin(user.email): transaction = ( query .filter(Transaction.transaction_id == transaction_id) .first() ) if not transaction: raise HTTPException( status_code=404, detail="Transaction not found." ) else: transaction = ( query .join(TransactionBreakdown, Transaction.transaction_id == TransactionBreakdown.transaction_id) .join(TransactionAccount, TransactionBreakdown.transaction_account_id == TransactionAccount.transaction_account_id) .filter(Transaction.transaction_id == transaction_id) .filter(TransactionAccount.user_id == user.user_id) .first() ) if not transaction: raise HTTPException( status_code=404, detail="Transaction not found or access denied." ) db.delete(transaction) db.commit() return {"message": "Transaction deleted successfully"} @app.post("/tags/", response_model=TagResponse) def create_tag( tag: TagCreate, user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """ Create a tag associated with the logged-in user by linking it to a placeholder transaction. """ # Create the tag new_tag = Tag(tag_name=tag.tag_name) db.add(new_tag) db.commit() db.refresh(new_tag) # Create a dummy transaction linked to the user's first account user_account = ( db.query(TransactionAccount) .filter(TransactionAccount.user_id == user.user_id) .first() ) if not user_account: raise HTTPException( status_code=403, detail="No account available to associate with the tag." ) # Associate the tag with a dummy transaction for the user dummy_transaction = Transaction( transaction_name=f"Tag_{new_tag.tag_id}_placeholder", amount=0, net_amount=0, date=datetime.utcnow(), ) db.add(dummy_transaction) db.commit() db.refresh(dummy_transaction) # Link the dummy transaction to the user's account dummy_breakdown = TransactionBreakdown( transaction_id=dummy_transaction.transaction_id, transaction_account_id=user_account.transaction_account_id, earned_amount=0, spent_amount=0, ) db.add(dummy_breakdown) # Associate the tag with the dummy transaction tag_assignment = TagAssignedToTransaction( transaction_id=dummy_transaction.transaction_id, tag_id=new_tag.tag_id, ) db.add(tag_assignment) db.commit() return new_tag @app.get("/tags/", response_model=List[TagResponse]) def get_tags( user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """ Admins can fetch all tags Regular users can retrieve tags accessible to the logged-in user based on their transactions. """ if is_admin(user.email): return db.query(Tag).all() accessible_tags = ( db.query(Tag) .join(TagAssignedToTransaction, Tag.tag_id == TagAssignedToTransaction.tag_id) .join(Transaction, TagAssignedToTransaction.transaction_id == Transaction.transaction_id) .join(TransactionBreakdown, Transaction.transaction_id == TransactionBreakdown.transaction_id) .join(TransactionAccount, TransactionBreakdown.transaction_account_id == TransactionAccount.transaction_account_id) .filter(TransactionAccount.user_id == user.user_id) .distinct() .all() ) return accessible_tags @app.post("/tags/assign/", response_model=dict) def assign_tag_to_transaction( tag_assign: TagAssign, user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """ Assign a tag to a transaction. - Admins can assign any tag to any transaction. - Regular users can assign a tag if: - The transaction belongs to them. - The tag is accessible (created by them or linked to their transactions). """ # Ensure the transaction belongs to the logged-in user transaction = ( db.query(Transaction) .join(TransactionBreakdown, Transaction.transaction_id == TransactionBreakdown.transaction_id) .join(TransactionAccount, TransactionBreakdown.transaction_account_id == TransactionAccount.transaction_account_id) .filter(Transaction.transaction_id == tag_assign.transaction_id) .filter(TransactionAccount.user_id == user.user_id) .first() ) if not transaction: raise HTTPException(status_code=404, detail="Transaction not found or access denied.") # Ensure the tag is accessible to the logged-in user tag_accessible = ( db.query(Tag) .join(TagAssignedToTransaction, Tag.tag_id == TagAssignedToTransaction.tag_id, isouter=True) .join(Transaction, TagAssignedToTransaction.transaction_id == Transaction.transaction_id, isouter=True) .join(TransactionBreakdown, Transaction.transaction_id == TransactionBreakdown.transaction_id, isouter=True) .join(TransactionAccount, TransactionBreakdown.transaction_account_id == TransactionAccount.transaction_account_id, isouter=True) .filter(Tag.tag_id == tag_assign.tag_id) .filter( (TransactionAccount.user_id == user.user_id) | # Tag linked to the user's transactions (TransactionAccount.user_id.is_(None)) # Newly created tag not yet assigned ) .first() ) if not tag_accessible: raise HTTPException( status_code=404, detail="Access denied to the tag." ) # Check if the tag is already assigned to the transaction existing_assignment = ( db.query(TagAssignedToTransaction) .filter( TagAssignedToTransaction.transaction_id == tag_assign.transaction_id, TagAssignedToTransaction.tag_id == tag_assign.tag_id, ) .first() ) if existing_assignment: raise HTTPException( status_code=400, detail="Tag already assigned to this transaction." ) # Assign the tag to the transaction assignment = TagAssignedToTransaction( transaction_id=tag_assign.transaction_id, tag_id=tag_assign.tag_id, ) db.add(assignment) db.commit() return {"message": "Tag assigned to transaction successfully"} @app.get("/tags/transaction/{transaction_id}", response_model=List[TagResponse]) def get_transaction_tags_for_user( transaction_id: int, user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """ Retrieve tags for a specific transaction. - Admins can retrieve tags for any transaction. - Regular users can retrieve tags if the transaction belongs to them. """ # Admins can access tags for any transaction if is_admin(user.email): tags = ( db.query(Tag) .join(TagAssignedToTransaction, Tag.tag_id == TagAssignedToTransaction.tag_id) .filter(TagAssignedToTransaction.transaction_id == transaction_id) .all() ) return tags # Check if the transaction belongs to the user transaction = ( db.query(Transaction) .join(TransactionBreakdown, Transaction.transaction_id == TransactionBreakdown.transaction_id) .join(TransactionAccount, TransactionBreakdown.transaction_account_id == TransactionAccount.transaction_account_id) .filter(Transaction.transaction_id == transaction_id) .filter(TransactionAccount.user_id == user.user_id) .first() ) if not transaction: raise HTTPException( status_code=403, detail="Access denied" ) # Retrieve tags for the transaction tags = ( db.query(Tag) .join(TagAssignedToTransaction, Tag.tag_id == TagAssignedToTransaction.tag_id) .filter(TagAssignedToTransaction.transaction_id == transaction_id) .all() ) return tags @app.get("/reports/total-spending", response_model=dict) def get_total_spending( user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """ Calculate and return total spending for the logged-in user. - Admins can view total spending for all users. """ try: query = db.query( func .sum(Transaction.amount) .label("total_spent") ) if is_admin(user.email): # Admin: Total spending for all users total_spent = ( query .filter(Transaction.amount > 0) .scalar() ) else: # Regular User: Total spending for their accounts total_spent = ( query .join(TransactionBreakdown, Transaction.transaction_id == TransactionBreakdown.transaction_id) .join(TransactionAccount, TransactionBreakdown.transaction_account_id == TransactionAccount.transaction_account_id) .filter(TransactionAccount.user_id == user.user_id) .filter(Transaction.amount > 0) .scalar() ) return {"total_spent": total_spent or 0.0} except Exception as e: print(f"Error calculating total spending: {e}") raise HTTPException( status_code=500, detail="Failed to calculate total spending." ) @app.get("/reports/spending-by-category", response_model=dict) def get_spending_by_category( user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """ Calculate and return spending grouped by category (tags) for the logged-in user. - Admins can view spending by category for all users. """ try: # Base query query = db.query( Tag.tag_name, func.sum(Transaction.amount).label("total_spent") ).join( TagAssignedToTransaction, Tag.tag_id == TagAssignedToTransaction.tag_id ).join( Transaction, TagAssignedToTransaction.transaction_id == Transaction.transaction_id ).filter( Transaction.amount > 0 # Include only positive amounts ) # Apply filters for regular users if not is_admin(user.email): query = query.join( TransactionBreakdown, Transaction.transaction_id == TransactionBreakdown.transaction_id ).join( TransactionAccount, TransactionBreakdown.transaction_account_id == TransactionAccount.transaction_account_id ).filter( TransactionAccount.user_id == user.user_id ) # Group by tag and calculate the total spending for each category spending_by_category = query.group_by(Tag.tag_name).all() # Prepare the response as a dictionary response = {row.tag_name: float(row.total_spent or 0) for row in spending_by_category} return {"spending_by_category": response} except Exception as e: print(f"Error calculating spending by category: {e}") raise HTTPException( status_code=500, detail="Failed to calculate spending by category." ) @app.get("/reports/spending-by-date-range", response_model=dict) def get_spending_by_date_range( start_date: str, # Expecting date in 'YYYY-MM-DD' format end_date: str, user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """ Calculate and return spending within a specified date range for the logged-in user. - Admins can view spending within the date range for all users. """ try: # Convert input dates to `datetime` start_date_parsed = datetime.strptime(start_date, "%Y-%m-%d") end_date_parsed = datetime.strptime(end_date, "%Y-%m-%d") # Query base query = db.query(func.sum(Transaction.amount).label("total_spent")) if is_admin(user.email): # Admin: Total spending for all users within the date range total_spent = ( query .filter( Transaction.date >= start_date_parsed, Transaction.date <= end_date_parsed, Transaction.amount > 0 ) .scalar() ) else: # Regular User: Total spending within the date range for their accounts total_spent = ( query .join(TransactionBreakdown, Transaction.transaction_id == TransactionBreakdown.transaction_id) .join(TransactionAccount, TransactionBreakdown.transaction_account_id == TransactionAccount.transaction_account_id) .filter( TransactionAccount.user_id == user.user_id, Transaction.date >= start_date_parsed, Transaction.date <= end_date_parsed, Transaction.amount > 0 ) .scalar() ) # Return result return {"total_spent": total_spent or 0.0} except Exception as e: print(f"Error calculating spending by date range: {e}") raise HTTPException( status_code=500, detail="Failed to calculate spending by date range." ) @app.get("/reports/exceeding-transactions", response_model=List[dict]) def get_exceeding_transactions( account_name: Optional[str] = None, # Allow filtering by account name user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """ Retrieve a list of transactions that exceeded the balance of an account, sorted chronologically. - Admins can view for all users. - Regular users can view for their own accounts. """ # Define the subquery to calculate `calculated_balance` using a window function subquery = ( db.query( Transaction.transaction_id, Transaction.transaction_name, Transaction.date.label("transaction_date"), TransactionAccount.account_name, User.user_id, User.user_name, TransactionBreakdown.spent_amount.label("transaction_amount"), func.sum(TransactionBreakdown.earned_amount - TransactionBreakdown.spent_amount) .over( partition_by=TransactionBreakdown.transaction_account_id, order_by=Transaction.date ) .label("calculated_balance"), ) .join(TransactionAccount, TransactionAccount.transaction_account_id == TransactionBreakdown.transaction_account_id) .join(User, TransactionAccount.user_id == User.user_id) .join(Transaction, Transaction.transaction_id == TransactionBreakdown.transaction_id) .subquery() ) query = db.query( subquery.c.transaction_id, subquery.c.transaction_name, subquery.c.transaction_date, subquery.c.account_name, subquery.c.user_id, subquery.c.user_name, subquery.c.transaction_amount, subquery.c.calculated_balance, ).filter( subquery.c.transaction_amount > subquery.c.calculated_balance, # Filter where transaction amount exceeds balance subquery.c.transaction_amount > 0, # Filter for positive transactions ) if account_name: query = query.filter(subquery.c.account_name == account_name) # Apply user-specific filtering for non-admins if not is_admin(user.email): query = query.filter(subquery.c.user_id == user.user_id) # Order results query = query.order_by( subquery.c.user_id, subquery.c.account_name, subquery.c.transaction_date.desc(), ) # Execute the query and fetch results results = query.all() if not results: return [] # Prepare response response = [ { "user_id": row.user_id, "user_name": row.user_name, "account_name": row.account_name, "transaction_id": row.transaction_id, "transaction_name": row.transaction_name, "transaction_amount": row.transaction_amount, "transaction_date": row.transaction_date, "calculated_balance": row.calculated_balance, } for row in results ] return response @app.get("/reports/exceeding-current-balance", response_model=List[dict]) def get_exceeding_current_balance( user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """ Retrieve a list of transactions that exceed the current balance of accounts. - Admins can view for all users. - Regular users can view for their own accounts. """ query = ( db.query( User.user_id, User.user_name, TransactionAccount.account_name, TransactionAccount.balance.label("current_balance"), Transaction.transaction_id, Transaction.transaction_name, TransactionBreakdown.spent_amount.label("transaction_amount"), Transaction.date.label("transaction_date"), ) .join(TransactionAccount, TransactionAccount.user_id == User.user_id) .join(TransactionBreakdown, TransactionBreakdown.transaction_account_id == TransactionAccount.transaction_account_id) .join(Transaction, Transaction.transaction_id == TransactionBreakdown.transaction_id) .filter(TransactionBreakdown.spent_amount > TransactionAccount.balance) # Transactions exceeding account balance .filter(TransactionBreakdown.spent_amount > 0) # Positive transactions only ) # Apply user-specific filtering for non-admins if not is_admin(user.email): query = query.filter(TransactionAccount.user_id == user.user_id) # Order results results = query.order_by(User.user_id, TransactionAccount.account_name, Transaction.date.desc()).all() # Prepare response return [ { "user_id": row.user_id, "user_name": row.user_name, "account_name": row.account_name, "current_balance": float(row.current_balance), "transaction_id": row.transaction_id, "transaction_name": row.transaction_name, "transaction_amount": float(row.transaction_amount), "transaction_date": row.transaction_date, } for row in results ] @app.get("/reports/exceeding-total-balances", response_model=List[dict]) def get_exceeding_total_balances( user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """ Retrieve a chronological list of transactions that exceed the calculated total balances for all accounts. - Admins can view for all users. - Regular users can view for their own accounts. """ # Subquery to calculate the running balance using a window function subquery = ( db.query( User.user_id.label("user_id"), User.user_name.label("user_name"), Transaction.transaction_id.label("transaction_id"), Transaction.transaction_name.label("transaction_name"), Transaction.date.label("transaction_date"), TransactionBreakdown.spent_amount.label("transaction_amount"), func.sum(TransactionBreakdown.earned_amount - TransactionBreakdown.spent_amount) .over(partition_by=User.user_id, order_by=Transaction.date) .label("calculated_total_balance") ) .join(TransactionAccount, TransactionAccount.user_id == User.user_id) .join(TransactionBreakdown, TransactionBreakdown.transaction_account_id == TransactionAccount.transaction_account_id) .join(Transaction, Transaction.transaction_id == TransactionBreakdown.transaction_id) .filter(TransactionBreakdown.spent_amount > 0) # Only positive transactions .subquery() ) # Outer query to filter transactions exceeding the calculated balance query = db.query( subquery.c.user_id, subquery.c.user_name, subquery.c.transaction_id, subquery.c.transaction_name, subquery.c.transaction_date, subquery.c.transaction_amount, subquery.c.calculated_total_balance ).filter( subquery.c.transaction_amount > subquery.c.calculated_total_balance # Exceeds the total balance ) # Apply user-specific filtering for non-admin users if not is_admin(user.email): query = query.filter(subquery.c.user_id == user.user_id) # Execute the query and return results results = query.order_by(subquery.c.user_id, subquery.c.transaction_date.desc()).all() return [ { "user_id": row.user_id, "user_name": row.user_name, "transaction_id": row.transaction_id, "transaction_name": row.transaction_name, "transaction_date": row.transaction_date, "transaction_amount": float(row.transaction_amount), "calculated_total_balance": float(row.calculated_total_balance), } for row in results ] @app.get("/reports/exceeding-user-total-balance", response_model=List[dict]) def get_exceeding_user_total_balance( user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """ Retrieve a list of users whose transactions exceed the total balance of all their accounts. - Admins can view results for all users. - Regular users can only see their own data. """ # Subquery to calculate the total balance for each user total_balance_subquery = ( db.query( TransactionAccount.user_id.label("user_id"), func.sum(TransactionAccount.balance).label("total_balance") ) .group_by(TransactionAccount.user_id) .subquery() ) # Main query query = ( db.query( User.user_id, User.user_name, func.sum(TransactionBreakdown.spent_amount).label("total_transaction_amount"), total_balance_subquery.c.total_balance.label("user_total_balance") ) .join(TransactionAccount, TransactionAccount.user_id == User.user_id) .join(TransactionBreakdown, TransactionBreakdown.transaction_account_id == TransactionAccount.transaction_account_id) .join(Transaction, Transaction.transaction_id == TransactionBreakdown.transaction_id) .join(total_balance_subquery, total_balance_subquery.c.user_id == User.user_id) .filter(Transaction.date <= func.current_date()) # Only transactions up to the current date .group_by(User.user_id, User.user_name, total_balance_subquery.c.total_balance) .having(func.sum(TransactionBreakdown.spent_amount) > total_balance_subquery.c.total_balance) # Exceeds total balance .order_by(User.user_id) ) # Apply user-specific filtering for non-admins if not is_admin(user.email): query = query.filter(User.user_id == user.user_id) # Execute the query results = query.all() # Prepare response return [ { "user_id": row.user_id, "user_name": row.user_name, "total_transaction_amount": float(row.total_transaction_amount), "user_total_balance": float(row.user_total_balance), } for row in results ]