kamau1 commited on
Commit
d692297
·
1 Parent(s): 06b615f

fix: enrich ticket-expense endpoints with full ticket + assignment context and add my-expenses + my-stats endpoints

Browse files
src/app/api/v1/ticket_expenses.py CHANGED
@@ -352,6 +352,113 @@ def delete_expense(
352
  return None
353
 
354
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
355
  # ============================================
356
  # GET EXPENSE
357
  # ============================================
@@ -378,6 +485,8 @@ def get_expense(
378
  )
379
 
380
  response = TicketExpenseResponse.model_validate(expense)
 
 
381
  if expense.incurred_by_user:
382
  response.incurred_by_user_name = expense.incurred_by_user.name
383
  if expense.approved_by_user:
@@ -385,6 +494,16 @@ def get_expense(
385
  if expense.paid_to_user:
386
  response.paid_to_user_name = expense.paid_to_user.name
387
 
 
 
 
 
 
 
 
 
 
 
388
  return response
389
 
390
 
@@ -448,16 +567,29 @@ def list_expenses(
448
  page_size=page_size
449
  )
450
 
451
- # Build responses with user names
452
  expense_responses = []
453
  for expense in expenses:
454
  response = TicketExpenseResponse.model_validate(expense)
 
 
455
  if expense.incurred_by_user:
456
  response.incurred_by_user_name = expense.incurred_by_user.name
457
  if expense.approved_by_user:
458
  response.approved_by_user_name = expense.approved_by_user.name
459
  if expense.paid_to_user:
460
  response.paid_to_user_name = expense.paid_to_user.name
 
 
 
 
 
 
 
 
 
 
 
461
  expense_responses.append(response)
462
 
463
  pages = (total + page_size - 1) // page_size
 
352
  return None
353
 
354
 
355
+ # ============================================
356
+ # FIELD AGENT HELPER ENDPOINTS
357
+ # ============================================
358
+
359
+ @router.get(
360
+ "/my-expenses",
361
+ response_model=TicketExpenseListResponse,
362
+ summary="Get current user's expenses"
363
+ )
364
+ def get_my_expenses(
365
+ is_approved: Optional[bool] = Query(None, description="Filter by approval status"),
366
+ is_paid: Optional[bool] = Query(None, description="Filter by payment status"),
367
+ from_date: Optional[date] = Query(None, description="Filter from date"),
368
+ to_date: Optional[date] = Query(None, description="Filter to date"),
369
+ page: int = Query(1, ge=1, description="Page number"),
370
+ page_size: int = Query(100, ge=1, le=100, description="Items per page"),
371
+ db: Session = Depends(get_db),
372
+ current_user: User = Depends(get_current_user)
373
+ ):
374
+ """
375
+ Get all expenses for the current user.
376
+
377
+ **Purpose:** Field agents can see all their expenses in one place
378
+
379
+ **Returns:** List of user's expenses with full context (ticket info, approval/payment status)
380
+ """
381
+ expenses, total = TicketExpenseService.list_expenses(
382
+ db=db,
383
+ current_user=current_user,
384
+ user_id=current_user.id, # Force filter to current user
385
+ is_approved=is_approved,
386
+ is_paid=is_paid,
387
+ from_date=from_date,
388
+ to_date=to_date,
389
+ page=page,
390
+ page_size=page_size
391
+ )
392
+
393
+ # Build responses with contextual information
394
+ expense_responses = []
395
+ for expense in expenses:
396
+ response = TicketExpenseResponse.model_validate(expense)
397
+
398
+ # User names
399
+ if expense.incurred_by_user:
400
+ response.incurred_by_user_name = expense.incurred_by_user.name
401
+ if expense.approved_by_user:
402
+ response.approved_by_user_name = expense.approved_by_user.name
403
+ if expense.paid_to_user:
404
+ response.paid_to_user_name = expense.paid_to_user.name
405
+
406
+ # Ticket context
407
+ if expense.ticket:
408
+ response.ticket_reference = expense.ticket.ticket_reference
409
+ response.ticket_title = expense.ticket.title
410
+ response.ticket_status = expense.ticket.status
411
+
412
+ # Assignment context
413
+ if expense.ticket_assignment:
414
+ response.assignment_status = expense.ticket_assignment.status
415
+
416
+ expense_responses.append(response)
417
+
418
+ pages = (total + page_size - 1) // page_size
419
+
420
+ return TicketExpenseListResponse(
421
+ expenses=expense_responses,
422
+ total=total,
423
+ page=page,
424
+ page_size=page_size,
425
+ pages=pages
426
+ )
427
+
428
+
429
+ @router.get(
430
+ "/my-stats",
431
+ response_model=TicketExpenseStats,
432
+ summary="Get current user's expense statistics"
433
+ )
434
+ def get_my_expense_stats(
435
+ from_date: Optional[date] = Query(None, description="Filter from date"),
436
+ to_date: Optional[date] = Query(None, description="Filter to date"),
437
+ db: Session = Depends(get_db),
438
+ current_user: User = Depends(get_current_user)
439
+ ):
440
+ """
441
+ Get expense statistics for the current user.
442
+
443
+ **Purpose:** Field agents can see their own expense summary
444
+
445
+ **Returns:**
446
+ - Total expenses and amount
447
+ - Pending approval count and amount
448
+ - Approved/paid breakdown
449
+ - Category breakdown
450
+ """
451
+ stats = TicketExpenseService.get_expense_stats(
452
+ db=db,
453
+ current_user=current_user,
454
+ user_id=current_user.id, # Force filter to current user
455
+ from_date=from_date,
456
+ to_date=to_date
457
+ )
458
+
459
+ return stats
460
+
461
+
462
  # ============================================
463
  # GET EXPENSE
464
  # ============================================
 
485
  )
486
 
487
  response = TicketExpenseResponse.model_validate(expense)
488
+
489
+ # User names
490
  if expense.incurred_by_user:
491
  response.incurred_by_user_name = expense.incurred_by_user.name
492
  if expense.approved_by_user:
 
494
  if expense.paid_to_user:
495
  response.paid_to_user_name = expense.paid_to_user.name
496
 
497
+ # Ticket context
498
+ if expense.ticket:
499
+ response.ticket_reference = expense.ticket.ticket_reference
500
+ response.ticket_title = expense.ticket.title
501
+ response.ticket_status = expense.ticket.status
502
+
503
+ # Assignment context
504
+ if expense.ticket_assignment:
505
+ response.assignment_status = expense.ticket_assignment.status
506
+
507
  return response
508
 
509
 
 
567
  page_size=page_size
568
  )
569
 
570
+ # Build responses with user names and contextual information
571
  expense_responses = []
572
  for expense in expenses:
573
  response = TicketExpenseResponse.model_validate(expense)
574
+
575
+ # User names
576
  if expense.incurred_by_user:
577
  response.incurred_by_user_name = expense.incurred_by_user.name
578
  if expense.approved_by_user:
579
  response.approved_by_user_name = expense.approved_by_user.name
580
  if expense.paid_to_user:
581
  response.paid_to_user_name = expense.paid_to_user.name
582
+
583
+ # Ticket context
584
+ if expense.ticket:
585
+ response.ticket_reference = expense.ticket.ticket_reference
586
+ response.ticket_title = expense.ticket.title
587
+ response.ticket_status = expense.ticket.status
588
+
589
+ # Assignment context
590
+ if expense.ticket_assignment:
591
+ response.assignment_status = expense.ticket_assignment.status
592
+
593
  expense_responses.append(response)
594
 
595
  pages = (total + page_size - 1) // page_size
src/app/schemas/ticket_expense.py CHANGED
@@ -305,6 +305,12 @@ class TicketExpenseResponse(BaseModel):
305
  created_at: datetime
306
  updated_at: datetime
307
 
 
 
 
 
 
 
308
  class Config:
309
  from_attributes = True
310
 
 
305
  created_at: datetime
306
  updated_at: datetime
307
 
308
+ # Contextual information (populated by API)
309
+ ticket_reference: Optional[str] = None
310
+ ticket_title: Optional[str] = None
311
+ ticket_status: Optional[str] = None
312
+ assignment_status: Optional[str] = None
313
+
314
  class Config:
315
  from_attributes = True
316
 
src/app/services/ticket_expense_service.py CHANGED
@@ -635,7 +635,8 @@ class TicketExpenseService:
635
  joinedload(TicketExpense.incurred_by_user),
636
  joinedload(TicketExpense.approved_by_user),
637
  joinedload(TicketExpense.paid_to_user),
638
- joinedload(TicketExpense.ticket)
 
639
  ).filter(
640
  TicketExpense.id == expense_id,
641
  TicketExpense.deleted_at.is_(None)
@@ -689,7 +690,8 @@ class TicketExpenseService:
689
  joinedload(TicketExpense.incurred_by_user),
690
  joinedload(TicketExpense.approved_by_user),
691
  joinedload(TicketExpense.paid_to_user),
692
- joinedload(TicketExpense.ticket)
 
693
  ).filter(
694
  TicketExpense.deleted_at.is_(None)
695
  )
 
635
  joinedload(TicketExpense.incurred_by_user),
636
  joinedload(TicketExpense.approved_by_user),
637
  joinedload(TicketExpense.paid_to_user),
638
+ joinedload(TicketExpense.ticket),
639
+ joinedload(TicketExpense.ticket_assignment)
640
  ).filter(
641
  TicketExpense.id == expense_id,
642
  TicketExpense.deleted_at.is_(None)
 
690
  joinedload(TicketExpense.incurred_by_user),
691
  joinedload(TicketExpense.approved_by_user),
692
  joinedload(TicketExpense.paid_to_user),
693
+ joinedload(TicketExpense.ticket),
694
+ joinedload(TicketExpense.ticket_assignment)
695
  ).filter(
696
  TicketExpense.deleted_at.is_(None)
697
  )