source: backend_python/main.py@ aef0c04

prototype
Last change on this file since aef0c04 was aef0c04, checked in by Vasilaki Tocili <vasilakigorgi@…>, 7 months ago

fix: rename folders with underscores and accomodate changes for it in the files

  • Instead of using app, now the main.py uses the local files with

prefix of dot

  • database.py gets the database URL using a more explicit way of

loading dotnev variables

  • Fixed the instructions to use the correct names and added a Note for

the new sqlalchemy requirement to use postgresql instead of the
deprecated postgres at the start of a DATABASE_URL

  • Property mode set to 100644
File size: 38.0 KB
Line 
1from fastapi import FastAPI, Depends, HTTPException
2from sqlalchemy.orm import Session
3from .database import get_db
4from .models import User, TransactionAccount, Transaction, TransactionBreakdown, Tag, TagAssignedToTransaction
5from pydantic import BaseModel
6from typing import List, Optional
7from datetime import datetime
8from fastapi.security import OAuth2PasswordBearer
9from .auth import create_access_token, decode_access_token, is_admin, hash_password, verify_password
10from sqlalchemy import func, literal_column, select
11
12# Initialize FastAPI app
13app = FastAPI()
14
15oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login/")
16
17# Pydantic schemas for validation and response models
18class UserCreate(BaseModel):
19 user_name: str
20 email: str
21 password: str
22
23class UserResponse(BaseModel):
24 user_id: int
25 user_name: str
26 email: str
27
28 class Config:
29 from_attributes = True
30
31class TransactionAccountCreate(BaseModel):
32 account_name: str
33 balance: float
34
35class TransactionAccountResponse(BaseModel):
36 transaction_account_id: int
37 account_name: str
38 balance: float
39
40 class Config:
41 from_attributes = True
42
43class TransactionBreakdownResponse(BaseModel):
44 transaction_account_id: int
45 earned_amount: float
46 spent_amount: float
47
48 class Config:
49 from_attributes = True
50
51class TransactionCreateRequest(BaseModel):
52 transaction_name: str
53 amount: float = 0.0 # Default to 0 if not provided
54 date: Optional[datetime] = None # Default to None, will use current time if not provided
55 tag_id: Optional[int] = None # Optional tag
56 target_account_id: int # Mandatory target account
57 breakdowns: Optional[List[TransactionBreakdownResponse]] = None # Optional list of breakdowns
58
59
60class TransactionUpdate(BaseModel):
61 transaction_name: str = None
62 amount: float = None
63 net_amount: float = None
64 date: datetime = None
65
66class TransactionResponse(BaseModel):
67 transaction_id: int
68 transaction_name: str
69 amount: float
70 net_amount: float
71 date: datetime
72
73 class Config:
74 json_encoders = {
75 datetime: lambda v: v.isoformat() # Serialize datetime as ISO 8601 string
76 }
77 from_attributes = True
78
79class TagCreate(BaseModel):
80 tag_name: str
81
82class TagResponse(BaseModel):
83 tag_id: int
84 tag_name: str
85
86 class Config:
87 from_attributes = True
88
89class TagAssign(BaseModel):
90 transaction_id: int
91 tag_id: int
92
93class AuthRequest(BaseModel):
94 user_name: Optional[str] = None
95 email: str
96 password: str
97
98class AuthResponse(BaseModel):
99 access_token: str
100 token_type: str
101
102# Dependency to get the current user
103def get_current_user(
104 token: str = Depends(oauth2_scheme),
105 db: Session = Depends(get_db)
106):
107 """
108 Retrieves the current user based on the access token.
109 """
110 try:
111 payload = decode_access_token(token)
112 user_id = payload.get("sub")
113 if not user_id:
114 raise HTTPException(
115 status_code=401,
116 detail="Invalid authentication credentials",
117 headers={"WWW-Authenticate": "Bearer"}
118 )
119
120 user = db.query(User).filter(User.user_id == user_id).first()
121 if not user:
122 raise HTTPException(
123 status_code=401,
124 detail="Invalid authentication credentials",
125 headers={"WWW-Authenticate": "Bearer"}
126 )
127
128 return user
129 except Exception as e:
130 print(f"Error decoding token or fetching user: {e}")
131 raise HTTPException(
132 status_code=401,
133 detail="Invalid authentication credentials",
134 headers={"WWW-Authenticate": "Bearer"}
135 )
136
137# Routes
138@app.get("/")
139def read_root():
140 return {"message": "Welcome to the Fein Prototype API"}
141
142@app.post("/auth/register/", response_model=AuthResponse)
143def register(
144 auth_request: AuthRequest,
145 db: Session = Depends(get_db)
146):
147 # Check if email already exists
148 existing_user = db.query(User).filter(User.email == auth_request.email).first()
149 if existing_user:
150 raise HTTPException(
151 status_code=400,
152 detail="Email already registered"
153 )
154
155 # Hash password and create new user
156 hashed_password = hash_password(auth_request.password)
157 new_user = User(
158 user_name=auth_request.user_name,
159 email=auth_request.email,
160 password=hashed_password
161 )
162 db.add(new_user)
163 db.commit()
164 db.refresh(new_user)
165
166 # Return access token
167 access_token = create_access_token({"sub": new_user.user_id, "email": new_user.email})
168 return {"access_token": access_token, "token_type": "bearer"}
169
170@app.post("/auth/login/", response_model=AuthResponse)
171def login(
172 auth_request: AuthRequest,
173 db: Session = Depends(get_db)
174):
175 # Verify email and password
176 user = db.query(User).filter(User.email == auth_request.email).first()
177 if not user or not verify_password(auth_request.password, user.password):
178 raise HTTPException(
179 status_code=401,
180 detail="Invalid email or password"
181 )
182
183 # Return access token
184 access_token = create_access_token({"sub": user.user_id, "email": user.email})
185 return {"access_token": access_token, "token_type": "bearer"}
186
187@app.get("/admin/accounts/", response_model=List[TransactionAccountResponse])
188def admin_get_all_accounts(
189 user: User = Depends(get_current_user),
190 db: Session = Depends(get_db)
191):
192 """
193 Admin can fetch all transaction accounts.
194 """
195 if not is_admin(user.email):
196 raise HTTPException(
197 status_code=403,
198 detail="Access denied"
199 )
200 return db.query(TransactionAccount).all()
201
202
203@app.post("/accounts/", response_model=TransactionAccountResponse)
204def create_account(
205 account: TransactionAccountCreate,
206 user: User = Depends(get_current_user),
207 db: Session = Depends(get_db)
208):
209 new_account = TransactionAccount(
210 account_name=account.account_name,
211 balance=account.balance,
212 user_id=user.user_id
213 )
214 db.add(new_account)
215 db.commit()
216 db.refresh(new_account)
217 return new_account
218
219@app.get("/accounts/", response_model=List[TransactionAccountResponse])
220def get_accounts(
221 user: User = Depends(get_current_user),
222 db: Session = Depends(get_db)
223):
224 """
225 Admin can fetch all accounts, regular users only their accounts.
226 """
227 query = db.query(TransactionAccount)
228
229 if is_admin(user.email):
230 return query.all()
231
232 return query.filter(TransactionAccount.user_id == user.user_id).all()
233
234
235@app.post("/transactions/", response_model=TransactionResponse)
236def create_transaction(
237 transaction_request: TransactionCreateRequest,
238 user: User = Depends(get_current_user),
239 db: Session = Depends(get_db)
240):
241 """
242 Admins can create transactions for any account; regular users only for their accounts.
243 Create a transaction and associate it with the user's accounts via breakdowns.
244 """
245 # Admin bypasses ownership checks
246 if not is_admin(user.email):
247 # Validate target account ownership
248 target_account = db.query(TransactionAccount).filter(
249 TransactionAccount.transaction_account_id == transaction_request.target_account_id,
250 TransactionAccount.user_id == user.user_id
251 ).first()
252 if not target_account:
253 raise HTTPException(
254 status_code=403,
255 detail="Access denied to target account."
256 )
257
258 # Create transaction
259 new_transaction = Transaction(
260 transaction_name=transaction_request.transaction_name,
261 amount=transaction_request.amount,
262 net_amount=0.0, # Will be updated based on breakdowns
263 date=transaction_request.date or datetime.utcnow(), # Use current UTC time if not provided
264 )
265 db.add(new_transaction)
266 db.commit()
267 db.refresh(new_transaction)
268
269 # Associate a tag, if provided
270 if transaction_request.tag_id:
271 tag = db.query(Tag).filter(Tag.tag_id == transaction_request.tag_id).first()
272 if tag:
273 tag_assignment = TagAssignedToTransaction(
274 transaction_id=new_transaction.transaction_id,
275 tag_id=tag.tag_id
276 )
277 db.add(tag_assignment)
278
279 # Add breakdowns
280 net_amount = 0.0
281 if transaction_request.breakdowns:
282 for breakdown in transaction_request.breakdowns:
283 # Validate breakdown account ownership
284 breakdown_account = db.query(TransactionAccount).filter(
285 TransactionAccount.transaction_account_id == breakdown.transaction_account_id,
286 TransactionAccount.user_id == user.user_id
287 ).first()
288 if not breakdown_account:
289 raise HTTPException(
290 status_code=403,
291 detail=f"Access denied to breakdown account {breakdown.transaction_account_id}."
292 )
293
294 # Create breakdown
295 new_breakdown = TransactionBreakdown(
296 transaction_id=new_transaction.transaction_id,
297 transaction_account_id=breakdown.transaction_account_id,
298 earned_amount=breakdown.earned_amount,
299 spent_amount=breakdown.spent_amount
300 )
301 db.add(new_breakdown)
302
303 # Calculate net amount
304 net_amount += breakdown.earned_amount - breakdown.spent_amount
305
306 # Update transaction's net amount
307 new_transaction.net_amount = net_amount
308 db.commit()
309 db.refresh(new_transaction)
310
311 return new_transaction
312
313@app.get("/transactions/", response_model=List[TransactionResponse])
314def get_transactions(
315 user: User = Depends(get_current_user),
316 db: Session = Depends(get_db)
317):
318 """
319 Fetch transactions based on user role, excluding placeholder transactions.
320 - Admin: Fetch all non-placeholder transactions.
321 - Regular User: Fetch non-placeholder transactions tied to the user's accounts via transaction breakdowns.
322 """
323 query = db.query(Transaction)
324
325 if is_admin(user.email):
326 # Admins see all non-placeholder transactions
327 transactions = (
328 query
329 .filter(~Transaction.transaction_name.like("Tag_%_placeholder"))
330 .all()
331 )
332 else:
333 transactions = (
334 query
335 .join(TransactionBreakdown, Transaction.transaction_id == TransactionBreakdown.transaction_id)
336 .join(TransactionAccount, TransactionBreakdown.transaction_account_id == TransactionAccount.transaction_account_id)
337 .filter(TransactionAccount.user_id == user.user_id)
338 .filter(~Transaction.transaction_name.like("Tag_%_placeholder")) # Exclude placeholders
339 .all()
340 )
341
342 return [
343 {
344 **transaction.__dict__,
345 "date": transaction.date.isoformat() # Convert datetime to ISO 8601 string
346 }
347 for transaction in transactions
348 ]
349
350@app.get("/transactions/{transaction_id}", response_model=TransactionResponse)
351def get_transaction_by_id(
352 transaction_id: int,
353 user: User = Depends(get_current_user),
354 db: Session = Depends(get_db)
355):
356 """
357 Retrieve a single transaction by its ID, ensuring access is restricted
358 to the transaction creator or an admin user.
359 """
360 # If the user is an admin, they can access any transaction
361 if is_admin(user.email):
362 transaction = db.query(Transaction).filter(Transaction.transaction_id == transaction_id).first()
363 if not transaction:
364 raise HTTPException(
365 status_code=404,
366 detail="Transaction not found."
367 )
368 return transaction
369
370 # Otherwise, restrict access to the transaction creator
371 transaction = (
372 db.query(Transaction)
373 .join(TransactionBreakdown, Transaction.transaction_id == TransactionBreakdown.transaction_id)
374 .join(TransactionAccount, TransactionBreakdown.transaction_account_id == TransactionAccount.transaction_account_id)
375 .filter(Transaction.transaction_id == transaction_id)
376 .filter(TransactionAccount.user_id == user.user_id)
377 .first()
378 )
379 if not transaction:
380 raise HTTPException(
381 status_code=404,
382 detail="Transaction not found or access denied."
383 )
384
385 return transaction
386
387@app.get("/transactions/{transaction_id}/breakdowns", response_model=List[TransactionBreakdownResponse])
388def get_transaction_breakdowns(
389 transaction_id: int,
390 user: User = Depends(get_current_user),
391 db: Session = Depends(get_db)
392):
393 """
394 Fetch transaction breakdowns for a specific transaction.
395 """
396 breakdowns = (
397 db.query(TransactionBreakdown)
398 .join(TransactionAccount, TransactionBreakdown.transaction_account_id == TransactionAccount.transaction_account_id)
399 .filter(TransactionBreakdown.transaction_id == transaction_id)
400 .filter(TransactionAccount.user_id == user.user_id)
401 .all()
402 )
403
404 if not breakdowns:
405 raise HTTPException(
406 status_code=404,
407 detail="No breakdowns found for this transaction."
408 )
409
410 return breakdowns
411
412
413@app.put("/transactions/{transaction_id}", response_model=TransactionResponse)
414def update_transaction(
415 transaction_id: int,
416 transaction_update: TransactionUpdate,
417 user: User = Depends(get_current_user),
418 db: Session = Depends(get_db)
419):
420 """
421 Admins can update any transaction
422 Regular users update a transaction only if it belongs to the logged-in user.
423 """
424 query = db.query(Transaction)
425
426 if is_admin(user.email):
427 transaction = (
428 query
429 .filter(Transaction.transaction_id == transaction_id)
430 .first()
431 )
432 if not transaction:
433 raise HTTPException(
434 status_code=404,
435 detail="Transaction not found."
436 )
437 else:
438 transaction = (
439 query
440 .join(TransactionBreakdown, Transaction.transaction_id == TransactionBreakdown.transaction_id)
441 .join(TransactionAccount, TransactionBreakdown.transaction_account_id == TransactionAccount.transaction_account_id)
442 .filter(Transaction.transaction_id == transaction_id)
443 .filter(TransactionAccount.user_id == user.user_id)
444 .first()
445 )
446 if not transaction:
447 raise HTTPException(
448 status_code=404,
449 detail="Transaction not found or access denied."
450 )
451
452 # Update transaction fields
453 for key, value in transaction_update.dict(exclude_unset=True).items():
454 setattr(transaction, key, value)
455
456 db.commit()
457 db.refresh(transaction)
458 return transaction
459
460@app.delete("/transactions/{transaction_id}")
461def delete_transaction(
462 transaction_id: int,
463 user: User = Depends(get_current_user),
464 db: Session = Depends(get_db)
465):
466 """
467 Admins can delete any transaction
468 Regular users can delete a transaction only if it belongs to the logged-in user.
469 """
470 query = db.query(Transaction)
471
472 if is_admin(user.email):
473 transaction = (
474 query
475 .filter(Transaction.transaction_id == transaction_id)
476 .first()
477 )
478 if not transaction:
479 raise HTTPException(
480 status_code=404,
481 detail="Transaction not found."
482 )
483 else:
484 transaction = (
485 query
486 .join(TransactionBreakdown, Transaction.transaction_id == TransactionBreakdown.transaction_id)
487 .join(TransactionAccount, TransactionBreakdown.transaction_account_id == TransactionAccount.transaction_account_id)
488 .filter(Transaction.transaction_id == transaction_id)
489 .filter(TransactionAccount.user_id == user.user_id)
490 .first()
491 )
492 if not transaction:
493 raise HTTPException(
494 status_code=404,
495 detail="Transaction not found or access denied."
496 )
497
498 db.delete(transaction)
499 db.commit()
500 return {"message": "Transaction deleted successfully"}
501
502
503@app.post("/tags/", response_model=TagResponse)
504def create_tag(
505 tag: TagCreate,
506 user: User = Depends(get_current_user),
507 db: Session = Depends(get_db)
508):
509 """
510 Create a tag associated with the logged-in user by linking it to a placeholder transaction.
511 """
512 # Create the tag
513 new_tag = Tag(tag_name=tag.tag_name)
514 db.add(new_tag)
515 db.commit()
516 db.refresh(new_tag)
517
518 # Create a dummy transaction linked to the user's first account
519 user_account = (
520 db.query(TransactionAccount)
521 .filter(TransactionAccount.user_id == user.user_id)
522 .first()
523 )
524 if not user_account:
525 raise HTTPException(
526 status_code=403,
527 detail="No account available to associate with the tag."
528 )
529
530 # Associate the tag with a dummy transaction for the user
531 dummy_transaction = Transaction(
532 transaction_name=f"Tag_{new_tag.tag_id}_placeholder",
533 amount=0,
534 net_amount=0,
535 date=datetime.utcnow(),
536 )
537 db.add(dummy_transaction)
538 db.commit()
539 db.refresh(dummy_transaction)
540
541 # Link the dummy transaction to the user's account
542 dummy_breakdown = TransactionBreakdown(
543 transaction_id=dummy_transaction.transaction_id,
544 transaction_account_id=user_account.transaction_account_id,
545 earned_amount=0,
546 spent_amount=0,
547 )
548 db.add(dummy_breakdown)
549
550 # Associate the tag with the dummy transaction
551 tag_assignment = TagAssignedToTransaction(
552 transaction_id=dummy_transaction.transaction_id,
553 tag_id=new_tag.tag_id,
554 )
555 db.add(tag_assignment)
556 db.commit()
557
558 return new_tag
559
560@app.get("/tags/", response_model=List[TagResponse])
561def get_tags(
562 user: User = Depends(get_current_user),
563 db: Session = Depends(get_db)
564):
565 """
566 Admins can fetch all tags
567 Regular users can retrieve tags accessible to the logged-in user based on their transactions.
568 """
569 if is_admin(user.email):
570 return db.query(Tag).all()
571
572 accessible_tags = (
573 db.query(Tag)
574 .join(TagAssignedToTransaction, Tag.tag_id == TagAssignedToTransaction.tag_id)
575 .join(Transaction, TagAssignedToTransaction.transaction_id == Transaction.transaction_id)
576 .join(TransactionBreakdown, Transaction.transaction_id == TransactionBreakdown.transaction_id)
577 .join(TransactionAccount, TransactionBreakdown.transaction_account_id == TransactionAccount.transaction_account_id)
578 .filter(TransactionAccount.user_id == user.user_id)
579 .distinct()
580 .all()
581 )
582 return accessible_tags
583
584@app.post("/tags/assign/", response_model=dict)
585def assign_tag_to_transaction(
586 tag_assign: TagAssign,
587 user: User = Depends(get_current_user),
588 db: Session = Depends(get_db)
589):
590 """
591 Assign a tag to a transaction.
592 - Admins can assign any tag to any transaction.
593 - Regular users can assign a tag if:
594 - The transaction belongs to them.
595 - The tag is accessible (created by them or linked to their transactions).
596 """
597 # Ensure the transaction belongs to the logged-in user
598 transaction = (
599 db.query(Transaction)
600 .join(TransactionBreakdown, Transaction.transaction_id == TransactionBreakdown.transaction_id)
601 .join(TransactionAccount, TransactionBreakdown.transaction_account_id == TransactionAccount.transaction_account_id)
602 .filter(Transaction.transaction_id == tag_assign.transaction_id)
603 .filter(TransactionAccount.user_id == user.user_id)
604 .first()
605 )
606 if not transaction:
607 raise HTTPException(status_code=404, detail="Transaction not found or access denied.")
608
609 # Ensure the tag is accessible to the logged-in user
610 tag_accessible = (
611 db.query(Tag)
612 .join(TagAssignedToTransaction, Tag.tag_id == TagAssignedToTransaction.tag_id, isouter=True)
613 .join(Transaction, TagAssignedToTransaction.transaction_id == Transaction.transaction_id, isouter=True)
614 .join(TransactionBreakdown, Transaction.transaction_id == TransactionBreakdown.transaction_id, isouter=True)
615 .join(TransactionAccount, TransactionBreakdown.transaction_account_id == TransactionAccount.transaction_account_id, isouter=True)
616 .filter(Tag.tag_id == tag_assign.tag_id)
617 .filter(
618 (TransactionAccount.user_id == user.user_id) | # Tag linked to the user's transactions
619 (TransactionAccount.user_id.is_(None)) # Newly created tag not yet assigned
620 )
621 .first()
622 )
623 if not tag_accessible:
624 raise HTTPException(
625 status_code=404,
626 detail="Access denied to the tag."
627 )
628
629 # Check if the tag is already assigned to the transaction
630 existing_assignment = (
631 db.query(TagAssignedToTransaction)
632 .filter(
633 TagAssignedToTransaction.transaction_id == tag_assign.transaction_id,
634 TagAssignedToTransaction.tag_id == tag_assign.tag_id,
635 )
636 .first()
637 )
638 if existing_assignment:
639 raise HTTPException(
640 status_code=400,
641 detail="Tag already assigned to this transaction."
642 )
643
644 # Assign the tag to the transaction
645 assignment = TagAssignedToTransaction(
646 transaction_id=tag_assign.transaction_id,
647 tag_id=tag_assign.tag_id,
648 )
649 db.add(assignment)
650 db.commit()
651
652 return {"message": "Tag assigned to transaction successfully"}
653
654@app.get("/tags/transaction/{transaction_id}", response_model=List[TagResponse])
655def get_transaction_tags_for_user(
656 transaction_id: int,
657 user: User = Depends(get_current_user),
658 db: Session = Depends(get_db)
659):
660 """
661 Retrieve tags for a specific transaction.
662 - Admins can retrieve tags for any transaction.
663 - Regular users can retrieve tags if the transaction belongs to them.
664 """
665 # Admins can access tags for any transaction
666 if is_admin(user.email):
667 tags = (
668 db.query(Tag)
669 .join(TagAssignedToTransaction, Tag.tag_id == TagAssignedToTransaction.tag_id)
670 .filter(TagAssignedToTransaction.transaction_id == transaction_id)
671 .all()
672 )
673 return tags
674
675 # Check if the transaction belongs to the user
676 transaction = (
677 db.query(Transaction)
678 .join(TransactionBreakdown, Transaction.transaction_id == TransactionBreakdown.transaction_id)
679 .join(TransactionAccount, TransactionBreakdown.transaction_account_id == TransactionAccount.transaction_account_id)
680 .filter(Transaction.transaction_id == transaction_id)
681 .filter(TransactionAccount.user_id == user.user_id)
682 .first()
683 )
684 if not transaction:
685 raise HTTPException(
686 status_code=403,
687 detail="Access denied"
688 )
689
690 # Retrieve tags for the transaction
691 tags = (
692 db.query(Tag)
693 .join(TagAssignedToTransaction, Tag.tag_id == TagAssignedToTransaction.tag_id)
694 .filter(TagAssignedToTransaction.transaction_id == transaction_id)
695 .all()
696 )
697
698 return tags
699
700@app.get("/reports/total-spending", response_model=dict)
701def get_total_spending(
702 user: User = Depends(get_current_user),
703 db: Session = Depends(get_db)
704):
705 """
706 Calculate and return total spending for the logged-in user.
707 - Admins can view total spending for all users.
708 """
709 try:
710 query = db.query(
711 func
712 .sum(Transaction.amount)
713 .label("total_spent")
714 )
715
716 if is_admin(user.email):
717 # Admin: Total spending for all users
718 total_spent = (
719 query
720 .filter(Transaction.amount > 0)
721 .scalar()
722 )
723 else:
724 # Regular User: Total spending for their accounts
725 total_spent = (
726 query
727 .join(TransactionBreakdown, Transaction.transaction_id == TransactionBreakdown.transaction_id)
728 .join(TransactionAccount, TransactionBreakdown.transaction_account_id == TransactionAccount.transaction_account_id)
729 .filter(TransactionAccount.user_id == user.user_id)
730 .filter(Transaction.amount > 0)
731 .scalar()
732 )
733
734 return {"total_spent": total_spent or 0.0}
735 except Exception as e:
736 print(f"Error calculating total spending: {e}")
737 raise HTTPException(
738 status_code=500,
739 detail="Failed to calculate total spending."
740 )
741
742@app.get("/reports/spending-by-category", response_model=dict)
743def get_spending_by_category(
744 user: User = Depends(get_current_user),
745 db: Session = Depends(get_db)
746):
747 """
748 Calculate and return spending grouped by category (tags) for the logged-in user.
749 - Admins can view spending by category for all users.
750 """
751 try:
752 # Base query
753 query = db.query(
754 Tag.tag_name,
755 func.sum(Transaction.amount).label("total_spent")
756 ).join(
757 TagAssignedToTransaction, Tag.tag_id == TagAssignedToTransaction.tag_id
758 ).join(
759 Transaction, TagAssignedToTransaction.transaction_id == Transaction.transaction_id
760 ).filter(
761 Transaction.amount > 0 # Include only positive amounts
762 )
763
764 # Apply filters for regular users
765 if not is_admin(user.email):
766 query = query.join(
767 TransactionBreakdown, Transaction.transaction_id == TransactionBreakdown.transaction_id
768 ).join(
769 TransactionAccount, TransactionBreakdown.transaction_account_id == TransactionAccount.transaction_account_id
770 ).filter(
771 TransactionAccount.user_id == user.user_id
772 )
773
774 # Group by tag and calculate the total spending for each category
775 spending_by_category = query.group_by(Tag.tag_name).all()
776
777 # Prepare the response as a dictionary
778 response = {row.tag_name: float(row.total_spent or 0) for row in spending_by_category}
779 return {"spending_by_category": response}
780
781 except Exception as e:
782 print(f"Error calculating spending by category: {e}")
783 raise HTTPException(
784 status_code=500,
785 detail="Failed to calculate spending by category."
786 )
787
788@app.get("/reports/spending-by-date-range", response_model=dict)
789def get_spending_by_date_range(
790 start_date: str, # Expecting date in 'YYYY-MM-DD' format
791 end_date: str,
792 user: User = Depends(get_current_user),
793 db: Session = Depends(get_db)
794):
795 """
796 Calculate and return spending within a specified date range for the logged-in user.
797 - Admins can view spending within the date range for all users.
798 """
799 try:
800 # Convert input dates to `datetime`
801 start_date_parsed = datetime.strptime(start_date, "%Y-%m-%d")
802 end_date_parsed = datetime.strptime(end_date, "%Y-%m-%d")
803
804 # Query base
805 query = db.query(func.sum(Transaction.amount).label("total_spent"))
806
807 if is_admin(user.email):
808 # Admin: Total spending for all users within the date range
809 total_spent = (
810 query
811 .filter(
812 Transaction.date >= start_date_parsed,
813 Transaction.date <= end_date_parsed,
814 Transaction.amount > 0
815 )
816 .scalar()
817 )
818 else:
819 # Regular User: Total spending within the date range for their accounts
820 total_spent = (
821 query
822 .join(TransactionBreakdown, Transaction.transaction_id == TransactionBreakdown.transaction_id)
823 .join(TransactionAccount, TransactionBreakdown.transaction_account_id == TransactionAccount.transaction_account_id)
824 .filter(
825 TransactionAccount.user_id == user.user_id,
826 Transaction.date >= start_date_parsed,
827 Transaction.date <= end_date_parsed,
828 Transaction.amount > 0
829 )
830 .scalar()
831 )
832
833 # Return result
834 return {"total_spent": total_spent or 0.0}
835 except Exception as e:
836 print(f"Error calculating spending by date range: {e}")
837 raise HTTPException(
838 status_code=500,
839 detail="Failed to calculate spending by date range."
840 )
841
842@app.get("/reports/exceeding-transactions", response_model=List[dict])
843def get_exceeding_transactions(
844 account_name: Optional[str] = None, # Allow filtering by account name
845 user: User = Depends(get_current_user),
846 db: Session = Depends(get_db)
847):
848 """
849 Retrieve a list of transactions that exceeded the balance of an account, sorted chronologically.
850 - Admins can view for all users.
851 - Regular users can view for their own accounts.
852 """
853 # Define the subquery to calculate `calculated_balance` using a window function
854 subquery = (
855 db.query(
856 Transaction.transaction_id,
857 Transaction.transaction_name,
858 Transaction.date.label("transaction_date"),
859 TransactionAccount.account_name,
860 User.user_id,
861 User.user_name,
862 TransactionBreakdown.spent_amount.label("transaction_amount"),
863 func.sum(TransactionBreakdown.earned_amount - TransactionBreakdown.spent_amount)
864 .over(
865 partition_by=TransactionBreakdown.transaction_account_id,
866 order_by=Transaction.date
867 )
868 .label("calculated_balance"),
869 )
870 .join(TransactionAccount, TransactionAccount.transaction_account_id == TransactionBreakdown.transaction_account_id)
871 .join(User, TransactionAccount.user_id == User.user_id)
872 .join(Transaction, Transaction.transaction_id == TransactionBreakdown.transaction_id)
873 .subquery()
874 )
875
876 query = db.query(
877 subquery.c.transaction_id,
878 subquery.c.transaction_name,
879 subquery.c.transaction_date,
880 subquery.c.account_name,
881 subquery.c.user_id,
882 subquery.c.user_name,
883 subquery.c.transaction_amount,
884 subquery.c.calculated_balance,
885 ).filter(
886 subquery.c.transaction_amount > subquery.c.calculated_balance, # Filter where transaction amount exceeds balance
887 subquery.c.transaction_amount > 0, # Filter for positive transactions
888 )
889
890 if account_name:
891 query = query.filter(subquery.c.account_name == account_name)
892
893 # Apply user-specific filtering for non-admins
894 if not is_admin(user.email):
895 query = query.filter(subquery.c.user_id == user.user_id)
896
897 # Order results
898 query = query.order_by(
899 subquery.c.user_id,
900 subquery.c.account_name,
901 subquery.c.transaction_date.desc(),
902 )
903
904 # Execute the query and fetch results
905 results = query.all()
906
907 if not results:
908 return []
909
910 # Prepare response
911 response = [
912 {
913 "user_id": row.user_id,
914 "user_name": row.user_name,
915 "account_name": row.account_name,
916 "transaction_id": row.transaction_id,
917 "transaction_name": row.transaction_name,
918 "transaction_amount": row.transaction_amount,
919 "transaction_date": row.transaction_date,
920 "calculated_balance": row.calculated_balance,
921 }
922 for row in results
923 ]
924
925 return response
926
927@app.get("/reports/exceeding-current-balance", response_model=List[dict])
928def get_exceeding_current_balance(
929 user: User = Depends(get_current_user),
930 db: Session = Depends(get_db)
931):
932 """
933 Retrieve a list of transactions that exceed the current balance of accounts.
934 - Admins can view for all users.
935 - Regular users can view for their own accounts.
936 """
937 query = (
938 db.query(
939 User.user_id,
940 User.user_name,
941 TransactionAccount.account_name,
942 TransactionAccount.balance.label("current_balance"),
943 Transaction.transaction_id,
944 Transaction.transaction_name,
945 TransactionBreakdown.spent_amount.label("transaction_amount"),
946 Transaction.date.label("transaction_date"),
947 )
948 .join(TransactionAccount, TransactionAccount.user_id == User.user_id)
949 .join(TransactionBreakdown, TransactionBreakdown.transaction_account_id == TransactionAccount.transaction_account_id)
950 .join(Transaction, Transaction.transaction_id == TransactionBreakdown.transaction_id)
951 .filter(TransactionBreakdown.spent_amount > TransactionAccount.balance) # Transactions exceeding account balance
952 .filter(TransactionBreakdown.spent_amount > 0) # Positive transactions only
953 )
954
955 # Apply user-specific filtering for non-admins
956 if not is_admin(user.email):
957 query = query.filter(TransactionAccount.user_id == user.user_id)
958
959 # Order results
960 results = query.order_by(User.user_id, TransactionAccount.account_name, Transaction.date.desc()).all()
961
962 # Prepare response
963 return [
964 {
965 "user_id": row.user_id,
966 "user_name": row.user_name,
967 "account_name": row.account_name,
968 "current_balance": float(row.current_balance),
969 "transaction_id": row.transaction_id,
970 "transaction_name": row.transaction_name,
971 "transaction_amount": float(row.transaction_amount),
972 "transaction_date": row.transaction_date,
973 }
974 for row in results
975 ]
976
977@app.get("/reports/exceeding-total-balances", response_model=List[dict])
978def get_exceeding_total_balances(
979 user: User = Depends(get_current_user),
980 db: Session = Depends(get_db)
981):
982 """
983 Retrieve a chronological list of transactions that exceed the calculated total balances for all accounts.
984 - Admins can view for all users.
985 - Regular users can view for their own accounts.
986 """
987 # Subquery to calculate the running balance using a window function
988 subquery = (
989 db.query(
990 User.user_id.label("user_id"),
991 User.user_name.label("user_name"),
992 Transaction.transaction_id.label("transaction_id"),
993 Transaction.transaction_name.label("transaction_name"),
994 Transaction.date.label("transaction_date"),
995 TransactionBreakdown.spent_amount.label("transaction_amount"),
996 func.sum(TransactionBreakdown.earned_amount - TransactionBreakdown.spent_amount)
997 .over(partition_by=User.user_id, order_by=Transaction.date)
998 .label("calculated_total_balance")
999 )
1000 .join(TransactionAccount, TransactionAccount.user_id == User.user_id)
1001 .join(TransactionBreakdown, TransactionBreakdown.transaction_account_id == TransactionAccount.transaction_account_id)
1002 .join(Transaction, Transaction.transaction_id == TransactionBreakdown.transaction_id)
1003 .filter(TransactionBreakdown.spent_amount > 0) # Only positive transactions
1004 .subquery()
1005 )
1006
1007 # Outer query to filter transactions exceeding the calculated balance
1008 query = db.query(
1009 subquery.c.user_id,
1010 subquery.c.user_name,
1011 subquery.c.transaction_id,
1012 subquery.c.transaction_name,
1013 subquery.c.transaction_date,
1014 subquery.c.transaction_amount,
1015 subquery.c.calculated_total_balance
1016 ).filter(
1017 subquery.c.transaction_amount > subquery.c.calculated_total_balance # Exceeds the total balance
1018 )
1019
1020 # Apply user-specific filtering for non-admin users
1021 if not is_admin(user.email):
1022 query = query.filter(subquery.c.user_id == user.user_id)
1023
1024 # Execute the query and return results
1025 results = query.order_by(subquery.c.user_id, subquery.c.transaction_date.desc()).all()
1026
1027 return [
1028 {
1029 "user_id": row.user_id,
1030 "user_name": row.user_name,
1031 "transaction_id": row.transaction_id,
1032 "transaction_name": row.transaction_name,
1033 "transaction_date": row.transaction_date,
1034 "transaction_amount": float(row.transaction_amount),
1035 "calculated_total_balance": float(row.calculated_total_balance),
1036 }
1037 for row in results
1038 ]
1039
1040@app.get("/reports/exceeding-user-total-balance", response_model=List[dict])
1041def get_exceeding_user_total_balance(
1042 user: User = Depends(get_current_user),
1043 db: Session = Depends(get_db)
1044):
1045 """
1046 Retrieve a list of users whose transactions exceed the total balance of all their accounts.
1047 - Admins can view results for all users.
1048 - Regular users can only see their own data.
1049 """
1050 # Subquery to calculate the total balance for each user
1051 total_balance_subquery = (
1052 db.query(
1053 TransactionAccount.user_id.label("user_id"),
1054 func.sum(TransactionAccount.balance).label("total_balance")
1055 )
1056 .group_by(TransactionAccount.user_id)
1057 .subquery()
1058 )
1059
1060 # Main query
1061 query = (
1062 db.query(
1063 User.user_id,
1064 User.user_name,
1065 func.sum(TransactionBreakdown.spent_amount).label("total_transaction_amount"),
1066 total_balance_subquery.c.total_balance.label("user_total_balance")
1067 )
1068 .join(TransactionAccount, TransactionAccount.user_id == User.user_id)
1069 .join(TransactionBreakdown, TransactionBreakdown.transaction_account_id == TransactionAccount.transaction_account_id)
1070 .join(Transaction, Transaction.transaction_id == TransactionBreakdown.transaction_id)
1071 .join(total_balance_subquery, total_balance_subquery.c.user_id == User.user_id)
1072 .filter(Transaction.date <= func.current_date()) # Only transactions up to the current date
1073 .group_by(User.user_id, User.user_name, total_balance_subquery.c.total_balance)
1074 .having(func.sum(TransactionBreakdown.spent_amount) > total_balance_subquery.c.total_balance) # Exceeds total balance
1075 .order_by(User.user_id)
1076 )
1077
1078 # Apply user-specific filtering for non-admins
1079 if not is_admin(user.email):
1080 query = query.filter(User.user_id == user.user_id)
1081
1082 # Execute the query
1083 results = query.all()
1084
1085 # Prepare response
1086 return [
1087 {
1088 "user_id": row.user_id,
1089 "user_name": row.user_name,
1090 "total_transaction_amount": float(row.total_transaction_amount),
1091 "user_total_balance": float(row.user_total_balance),
1092 }
1093 for row in results
1094 ]
1095
Note: See TracBrowser for help on using the repository browser.