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
RevLine 
[9a5d0df]1from fastapi import FastAPI, Depends, HTTPException
2from sqlalchemy.orm import Session
[aef0c04]3from .database import get_db
4from .models import User, TransactionAccount, Transaction, TransactionBreakdown, Tag, TagAssignedToTransaction
[9a5d0df]5from pydantic import BaseModel
6from typing import List, Optional
7from datetime import datetime
8from fastapi.security import OAuth2PasswordBearer
[aef0c04]9from .auth import create_access_token, decode_access_token, is_admin, hash_password, verify_password
[16e7e83]10from sqlalchemy import func, literal_column, select
[9a5d0df]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
[b8fed5c]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
[fe68975]51class TransactionCreateRequest(BaseModel):
[9a5d0df]52 transaction_name: str
[fe68975]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
[9a5d0df]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
[539afcd]103def get_current_user(
104 token: str = Depends(oauth2_scheme),
105 db: Session = Depends(get_db)
106):
[bf146db]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:
[539afcd]114 raise HTTPException(
115 status_code=401,
116 detail="Invalid authentication credentials",
117 headers={"WWW-Authenticate": "Bearer"}
118 )
[bf146db]119
120 user = db.query(User).filter(User.user_id == user_id).first()
121 if not user:
[539afcd]122 raise HTTPException(
123 status_code=401,
124 detail="Invalid authentication credentials",
125 headers={"WWW-Authenticate": "Bearer"}
126 )
[bf146db]127
128 return user
129 except Exception as e:
130 print(f"Error decoding token or fetching user: {e}")
[539afcd]131 raise HTTPException(
132 status_code=401,
133 detail="Invalid authentication credentials",
134 headers={"WWW-Authenticate": "Bearer"}
135 )
[9a5d0df]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)
[539afcd]143def register(
144 auth_request: AuthRequest,
145 db: Session = Depends(get_db)
146):
[9a5d0df]147 # Check if email already exists
148 existing_user = db.query(User).filter(User.email == auth_request.email).first()
149 if existing_user:
[539afcd]150 raise HTTPException(
151 status_code=400,
152 detail="Email already registered"
153 )
[9a5d0df]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
[bf146db]167 access_token = create_access_token({"sub": new_user.user_id, "email": new_user.email})
[9a5d0df]168 return {"access_token": access_token, "token_type": "bearer"}
169
170@app.post("/auth/login/", response_model=AuthResponse)
[539afcd]171def login(
172 auth_request: AuthRequest,
173 db: Session = Depends(get_db)
174):
[9a5d0df]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):
[539afcd]178 raise HTTPException(
179 status_code=401,
180 detail="Invalid email or password"
181 )
[9a5d0df]182
183 # Return access token
[bf146db]184 access_token = create_access_token({"sub": user.user_id, "email": user.email})
[9a5d0df]185 return {"access_token": access_token, "token_type": "bearer"}
186
187@app.get("/admin/accounts/", response_model=List[TransactionAccountResponse])
[539afcd]188def admin_get_all_accounts(
189 user: User = Depends(get_current_user),
190 db: Session = Depends(get_db)
191):
[6d70891]192 """
193 Admin can fetch all transaction accounts.
194 """
[9a5d0df]195 if not is_admin(user.email):
[539afcd]196 raise HTTPException(
197 status_code=403,
198 detail="Access denied"
199 )
[9a5d0df]200 return db.query(TransactionAccount).all()
201
202
203@app.post("/accounts/", response_model=TransactionAccountResponse)
[539afcd]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 )
[9a5d0df]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])
[539afcd]220def get_accounts(
221 user: User = Depends(get_current_user),
222 db: Session = Depends(get_db)
223):
[6d70891]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
[9a5d0df]234
235@app.post("/transactions/", response_model=TransactionResponse)
[539afcd]236def create_transaction(
237 transaction_request: TransactionCreateRequest,
238 user: User = Depends(get_current_user),
239 db: Session = Depends(get_db)
240):
[fe68975]241 """
[6d70891]242 Admins can create transactions for any account; regular users only for their accounts.
[fe68975]243 Create a transaction and associate it with the user's accounts via breakdowns.
244 """
[6d70891]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 )
[fe68975]257
258 # Create transaction
[9a5d0df]259 new_transaction = Transaction(
[fe68975]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
[9a5d0df]264 )
265 db.add(new_transaction)
266 db.commit()
267 db.refresh(new_transaction)
[fe68975]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:
[539afcd]289 raise HTTPException(
290 status_code=403,
291 detail=f"Access denied to breakdown account {breakdown.transaction_account_id}."
292 )
[fe68975]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
[9a5d0df]311 return new_transaction
312
313@app.get("/transactions/", response_model=List[TransactionResponse])
[9713fbe]314def get_transactions(
315 user: User = Depends(get_current_user),
316 db: Session = Depends(get_db)
317):
[b8fed5c]318 """
[9713fbe]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.
[b8fed5c]322 """
[9713fbe]323 query = db.query(Transaction)
324
[b8fed5c]325 if is_admin(user.email):
[9713fbe]326 # Admins see all non-placeholder transactions
327 transactions = (
328 query
329 .filter(~Transaction.transaction_name.like("Tag_%_placeholder"))
330 .all()
331 )
[b8fed5c]332 else:
333 transactions = (
[9713fbe]334 query
[b8fed5c]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)
[9713fbe]338 .filter(~Transaction.transaction_name.like("Tag_%_placeholder")) # Exclude placeholders
[b8fed5c]339 .all()
340 )
341
[9a5d0df]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
[88d7d3d]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:
[539afcd]364 raise HTTPException(
365 status_code=404,
366 detail="Transaction not found."
367 )
[88d7d3d]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:
[539afcd]380 raise HTTPException(
381 status_code=404,
382 detail="Transaction not found or access denied."
383 )
[88d7d3d]384
385 return transaction
386
[b8fed5c]387@app.get("/transactions/{transaction_id}/breakdowns", response_model=List[TransactionBreakdownResponse])
[539afcd]388def get_transaction_breakdowns(
389 transaction_id: int,
390 user: User = Depends(get_current_user),
391 db: Session = Depends(get_db)
392):
[b8fed5c]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:
[539afcd]405 raise HTTPException(
406 status_code=404,
407 detail="No breakdowns found for this transaction."
408 )
[b8fed5c]409
410 return breakdowns
411
412
[9a5d0df]413@app.put("/transactions/{transaction_id}", response_model=TransactionResponse)
[6bb334f]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 """
[6d70891]421 Admins can update any transaction
422 Regular users update a transaction only if it belongs to the logged-in user.
[6bb334f]423 """
[6d70891]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()
[539afcd]431 )
[6d70891]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 )
[9a5d0df]451
[6bb334f]452 # Update transaction fields
[9a5d0df]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}")
[6bb334f]461def delete_transaction(
462 transaction_id: int,
463 user: User = Depends(get_current_user),
464 db: Session = Depends(get_db)
465):
466 """
[6d70891]467 Admins can delete any transaction
468 Regular users can delete a transaction only if it belongs to the logged-in user.
[6bb334f]469 """
[6d70891]470 query = db.query(Transaction)
[6bb334f]471
[6d70891]472 if is_admin(user.email):
473 transaction = (
474 query
475 .filter(Transaction.transaction_id == transaction_id)
476 .first()
[539afcd]477 )
[6d70891]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 )
[9a5d0df]497
498 db.delete(transaction)
499 db.commit()
500 return {"message": "Transaction deleted successfully"}
501
502
503@app.post("/tags/", response_model=TagResponse)
[f7ceb2a]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
[9a5d0df]513 new_tag = Tag(tag_name=tag.tag_name)
514 db.add(new_tag)
515 db.commit()
516 db.refresh(new_tag)
[f7ceb2a]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:
[539afcd]525 raise HTTPException(
526 status_code=403,
527 detail="No account available to associate with the tag."
528 )
[f7ceb2a]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
[9a5d0df]558 return new_tag
559
560@app.get("/tags/", response_model=List[TagResponse])
[05f32c9]561def get_tags(
562 user: User = Depends(get_current_user),
563 db: Session = Depends(get_db)
564):
565 """
[6d70891]566 Admins can fetch all tags
567 Regular users can retrieve tags accessible to the logged-in user based on their transactions.
[05f32c9]568 """
[6d70891]569 if is_admin(user.email):
570 return db.query(Tag).all()
571
[05f32c9]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
[9a5d0df]583
584@app.post("/tags/assign/", response_model=dict)
[be05965]585def assign_tag_to_transaction(
586 tag_assign: TagAssign,
587 user: User = Depends(get_current_user),
588 db: Session = Depends(get_db)
589):
590 """
[6d70891]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).
[be05965]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.")
[9a5d0df]608
[be05965]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:
[539afcd]624 raise HTTPException(
625 status_code=404,
626 detail="Access denied to the tag."
627 )
[be05965]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:
[539afcd]639 raise HTTPException(
640 status_code=400,
641 detail="Tag already assigned to this transaction."
642 )
[9a5d0df]643
[be05965]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 )
[9a5d0df]649 db.add(assignment)
650 db.commit()
[be05965]651
[9a5d0df]652 return {"message": "Tag assigned to transaction successfully"}
653
[fe68975]654@app.get("/tags/transaction/{transaction_id}", response_model=List[TagResponse])
[890ac00]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 """
[6d70891]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.
[890ac00]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 )
[9a5d0df]684 if not transaction:
[539afcd]685 raise HTTPException(
686 status_code=403,
687 detail="Access denied"
688 )
[890ac00]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 )
[fe68975]697
[890ac00]698 return tags
[16e7e83]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
[04e01b0]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
[7fcd934]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
[fbcfd47]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 ]
[7fcd934]1039
[1fdde1a]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.