updates
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -3,6 +3,7 @@ Routes for financial audit trail access.
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.exc import ProgrammingError, OperationalError
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from ...shared.config.database import get_db
|
||||
@@ -87,24 +88,29 @@ async def get_financial_audit_trail(
|
||||
})
|
||||
|
||||
# Get total count for pagination
|
||||
total_query = db.query(FinancialAuditTrail)
|
||||
if payment_id:
|
||||
total_query = total_query.filter(FinancialAuditTrail.payment_id == payment_id)
|
||||
if invoice_id:
|
||||
total_query = total_query.filter(FinancialAuditTrail.invoice_id == invoice_id)
|
||||
if booking_id:
|
||||
total_query = total_query.filter(FinancialAuditTrail.booking_id == booking_id)
|
||||
if action_type_enum:
|
||||
total_query = total_query.filter(FinancialAuditTrail.action_type == action_type_enum)
|
||||
if user_id:
|
||||
total_query = total_query.filter(FinancialAuditTrail.performed_by == user_id)
|
||||
if start:
|
||||
total_query = total_query.filter(FinancialAuditTrail.created_at >= start)
|
||||
if end:
|
||||
total_query = total_query.filter(FinancialAuditTrail.created_at <= end)
|
||||
|
||||
total_count = total_query.count()
|
||||
total_pages = (total_count + limit - 1) // limit
|
||||
try:
|
||||
total_query = db.query(FinancialAuditTrail)
|
||||
if payment_id:
|
||||
total_query = total_query.filter(FinancialAuditTrail.payment_id == payment_id)
|
||||
if invoice_id:
|
||||
total_query = total_query.filter(FinancialAuditTrail.invoice_id == invoice_id)
|
||||
if booking_id:
|
||||
total_query = total_query.filter(FinancialAuditTrail.booking_id == booking_id)
|
||||
if action_type_enum:
|
||||
total_query = total_query.filter(FinancialAuditTrail.action_type == action_type_enum)
|
||||
if user_id:
|
||||
total_query = total_query.filter(FinancialAuditTrail.performed_by == user_id)
|
||||
if start:
|
||||
total_query = total_query.filter(FinancialAuditTrail.created_at >= start)
|
||||
if end:
|
||||
total_query = total_query.filter(FinancialAuditTrail.created_at <= end)
|
||||
|
||||
total_count = total_query.count()
|
||||
total_pages = (total_count + limit - 1) // limit
|
||||
except (ProgrammingError, OperationalError):
|
||||
# If table doesn't exist, count is 0
|
||||
total_count = 0
|
||||
total_pages = 0
|
||||
|
||||
return success_response(
|
||||
data={
|
||||
@@ -120,6 +126,26 @@ async def get_financial_audit_trail(
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except (ProgrammingError, OperationalError) as e:
|
||||
# Handle case where financial_audit_trail table doesn't exist
|
||||
error_msg = str(e)
|
||||
if 'doesn\'t exist' in error_msg or 'Table' in error_msg:
|
||||
logger.warning(f'Financial audit trail table not found: {error_msg}')
|
||||
return success_response(
|
||||
data={
|
||||
'audit_trail': [],
|
||||
'pagination': {
|
||||
'page': page,
|
||||
'limit': limit,
|
||||
'total': 0,
|
||||
'total_pages': 0
|
||||
},
|
||||
'note': 'Financial audit trail table not yet created. Please run database migrations.'
|
||||
},
|
||||
message='Financial audit trail is not available (table not created)'
|
||||
)
|
||||
logger.error(f'Database error retrieving financial audit trail: {str(e)}', exc_info=True)
|
||||
raise HTTPException(status_code=500, detail='Database error occurred while retrieving audit trail')
|
||||
except Exception as e:
|
||||
logger.error(f'Error retrieving financial audit trail: {str(e)}', exc_info=True)
|
||||
raise HTTPException(status_code=500, detail='An error occurred while retrieving audit trail')
|
||||
@@ -158,6 +184,14 @@ async def get_audit_record(
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except (ProgrammingError, OperationalError) as e:
|
||||
# Handle case where financial_audit_trail table doesn't exist
|
||||
error_msg = str(e)
|
||||
if 'doesn\'t exist' in error_msg or 'Table' in error_msg:
|
||||
logger.warning(f'Financial audit trail table not found: {error_msg}')
|
||||
raise HTTPException(status_code=404, detail='Financial audit trail table not yet created. Please run database migrations.')
|
||||
logger.error(f'Database error retrieving audit record: {str(e)}', exc_info=True)
|
||||
raise HTTPException(status_code=500, detail='Database error occurred while retrieving audit record')
|
||||
except Exception as e:
|
||||
logger.error(f'Error retrieving audit record: {str(e)}', exc_info=True)
|
||||
raise HTTPException(status_code=500, detail='An error occurred while retrieving audit record')
|
||||
|
||||
@@ -24,6 +24,14 @@ router = APIRouter(prefix='/invoices', tags=['invoices'])
|
||||
@router.get('/')
|
||||
async def get_invoices(request: Request, booking_id: Optional[int]=Query(None), status_filter: Optional[str]=Query(None, alias='status'), page: int=Query(1, ge=1), limit: int=Query(10, ge=1, le=100), current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||
try:
|
||||
# SECURITY: Verify booking ownership when booking_id is provided
|
||||
if booking_id and not can_access_all_invoices(current_user, db):
|
||||
booking = db.query(Booking).filter(Booking.id == booking_id).first()
|
||||
if not booking:
|
||||
raise HTTPException(status_code=404, detail='Booking not found')
|
||||
if booking.user_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail='Forbidden: You do not have permission to access invoices for this booking')
|
||||
|
||||
user_id = None if can_access_all_invoices(current_user, db) else current_user.id
|
||||
result = InvoiceService.get_invoices(db=db, user_id=user_id, booking_id=booking_id, status=status_filter, page=page, limit=limit)
|
||||
return success_response(data=result)
|
||||
@@ -38,8 +46,10 @@ async def get_invoice_by_id(id: int, current_user: User=Depends(get_current_user
|
||||
invoice = InvoiceService.get_invoice(id, db)
|
||||
if not invoice:
|
||||
raise HTTPException(status_code=404, detail='Invoice not found')
|
||||
if not can_access_all_invoices(current_user, db) and invoice['user_id'] != current_user.id:
|
||||
raise HTTPException(status_code=403, detail='Forbidden')
|
||||
# SECURITY: Verify invoice ownership for non-admin/accountant users
|
||||
if not can_access_all_invoices(current_user, db):
|
||||
if 'user_id' not in invoice or invoice['user_id'] != current_user.id:
|
||||
raise HTTPException(status_code=403, detail='Forbidden: You do not have permission to access this invoice')
|
||||
return success_response(data={'invoice': invoice})
|
||||
except HTTPException:
|
||||
raise
|
||||
@@ -48,9 +58,10 @@ async def get_invoice_by_id(id: int, current_user: User=Depends(get_current_user
|
||||
logger.error(f'Error fetching invoice by id: {str(e)}', exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.post('/')
|
||||
async def create_invoice(request: Request, invoice_data: CreateInvoiceRequest, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||
@router.post('/', dependencies=[Depends(authorize_roles('admin', 'staff', 'accountant'))])
|
||||
async def create_invoice(request: Request, invoice_data: CreateInvoiceRequest, current_user: User=Depends(authorize_roles('admin', 'staff', 'accountant')), db: Session=Depends(get_db)):
|
||||
try:
|
||||
# Defense in depth: Additional business logic check (authorize_roles already verified)
|
||||
if not can_create_invoices(current_user, db):
|
||||
raise HTTPException(status_code=403, detail='Forbidden')
|
||||
booking_id = invoice_data.booking_id
|
||||
@@ -137,13 +148,46 @@ async def mark_invoice_as_paid(request: Request, id: int, payment_data: MarkInvo
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.delete('/{id}')
|
||||
async def delete_invoice(id: int, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
|
||||
async def delete_invoice(id: int, request: Request, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
|
||||
client_ip = request.client.host if request.client else None
|
||||
user_agent = request.headers.get('User-Agent')
|
||||
request_id = get_request_id(request)
|
||||
|
||||
try:
|
||||
invoice = db.query(Invoice).filter(Invoice.id == id).first()
|
||||
if not invoice:
|
||||
raise HTTPException(status_code=404, detail='Invoice not found')
|
||||
|
||||
# Capture invoice info before deletion for audit
|
||||
deleted_invoice_info = {
|
||||
'invoice_id': invoice.id,
|
||||
'invoice_number': invoice.invoice_number,
|
||||
'user_id': invoice.user_id,
|
||||
'booking_id': invoice.booking_id,
|
||||
'total_amount': float(invoice.total_amount) if invoice.total_amount else 0.0,
|
||||
'status': invoice.status.value if hasattr(invoice.status, 'value') else str(invoice.status),
|
||||
}
|
||||
|
||||
db.delete(invoice)
|
||||
db.commit()
|
||||
|
||||
# SECURITY: Log invoice deletion for audit trail
|
||||
try:
|
||||
await audit_service.log_action(
|
||||
db=db,
|
||||
action='invoice_deleted',
|
||||
resource_type='invoice',
|
||||
user_id=current_user.id,
|
||||
resource_id=id,
|
||||
ip_address=client_ip,
|
||||
user_agent=user_agent,
|
||||
request_id=request_id,
|
||||
details=deleted_invoice_info,
|
||||
status='success'
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f'Failed to log invoice deletion audit: {e}')
|
||||
|
||||
return success_response(message='Invoice deleted successfully')
|
||||
except HTTPException:
|
||||
raise
|
||||
|
||||
@@ -79,6 +79,14 @@ async def cancel_booking_on_payment_failure(booking: Booking, db: Session, reaso
|
||||
@router.get('/')
|
||||
async def get_payments(booking_id: Optional[int]=Query(None), status_filter: Optional[str]=Query(None, alias='status'), page: int=Query(1, ge=1), limit: int=Query(10, ge=1, le=100), current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||
try:
|
||||
# SECURITY: Verify booking ownership when booking_id is provided
|
||||
if booking_id and not can_access_all_payments(current_user, db):
|
||||
booking = db.query(Booking).filter(Booking.id == booking_id).first()
|
||||
if not booking:
|
||||
raise HTTPException(status_code=404, detail='Booking not found')
|
||||
if booking.user_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail='Forbidden: You do not have permission to access payments for this booking')
|
||||
|
||||
if booking_id:
|
||||
query = db.query(Payment).filter(Payment.booking_id == booking_id)
|
||||
else:
|
||||
@@ -168,12 +176,16 @@ async def get_payments_by_booking_id(booking_id: int, current_user: User=Depends
|
||||
@router.get('/{id}')
|
||||
async def get_payment_by_id(id: int, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||
try:
|
||||
payment = db.query(Payment).filter(Payment.id == id).first()
|
||||
# SECURITY: Load booking relationship to verify ownership
|
||||
payment = db.query(Payment).options(joinedload(Payment.booking)).filter(Payment.id == id).first()
|
||||
if not payment:
|
||||
raise HTTPException(status_code=404, detail='Payment not found')
|
||||
# SECURITY: Verify payment ownership for non-admin/accountant users
|
||||
if not can_access_all_payments(current_user, db):
|
||||
if payment.booking and payment.booking.user_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail='Forbidden')
|
||||
if not payment.booking:
|
||||
raise HTTPException(status_code=403, detail='Forbidden: Payment does not belong to any booking')
|
||||
if payment.booking.user_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail='Forbidden: You do not have permission to access this payment')
|
||||
payment_dict = {'id': payment.id, 'booking_id': payment.booking_id, 'amount': float(payment.amount) if payment.amount else 0.0, 'payment_method': payment.payment_method.value if isinstance(payment.payment_method, PaymentMethod) else payment.payment_method, 'payment_type': payment.payment_type.value if isinstance(payment.payment_type, PaymentType) else payment.payment_type, 'deposit_percentage': payment.deposit_percentage, 'related_payment_id': payment.related_payment_id, 'payment_status': payment.payment_status.value if isinstance(payment.payment_status, PaymentStatus) else payment.payment_status, 'transaction_id': payment.transaction_id, 'payment_date': payment.payment_date.isoformat() if payment.payment_date else None, 'notes': payment.notes, 'created_at': payment.created_at.isoformat() if payment.created_at else None}
|
||||
if payment.booking:
|
||||
payment_dict['booking'] = {'id': payment.booking.id, 'booking_number': payment.booking.booking_number}
|
||||
|
||||
Binary file not shown.
@@ -2,6 +2,7 @@
|
||||
Service for creating and managing financial audit trail records.
|
||||
"""
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.exc import ProgrammingError, OperationalError
|
||||
from typing import Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
from ..models.financial_audit_trail import FinancialAuditTrail, FinancialActionType
|
||||
@@ -56,6 +57,15 @@ class FinancialAuditService:
|
||||
|
||||
logger.info(f"Financial audit trail created: {action_type.value} by user {performed_by}")
|
||||
return audit_record
|
||||
except (ProgrammingError, OperationalError) as e:
|
||||
error_msg = str(e)
|
||||
if 'doesn\'t exist' in error_msg or 'Table' in error_msg:
|
||||
logger.warning(f"Financial audit trail table not found, skipping audit logging: {error_msg}")
|
||||
# Don't fail the main operation if audit table doesn't exist
|
||||
return None
|
||||
logger.error(f"Database error creating financial audit trail: {str(e)}", exc_info=True)
|
||||
# Don't fail the main operation if audit logging fails
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating financial audit trail: {str(e)}", exc_info=True)
|
||||
# Don't fail the main operation if audit logging fails
|
||||
@@ -95,7 +105,14 @@ class FinancialAuditService:
|
||||
query = query.order_by(FinancialAuditTrail.created_at.desc())
|
||||
query = query.limit(limit).offset(offset)
|
||||
|
||||
return query.all()
|
||||
try:
|
||||
return query.all()
|
||||
except (ProgrammingError, OperationalError) as e:
|
||||
error_msg = str(e)
|
||||
if 'doesn\'t exist' in error_msg or 'Table' in error_msg:
|
||||
logger.warning(f"Financial audit trail table not found: {error_msg}")
|
||||
return []
|
||||
raise
|
||||
|
||||
|
||||
financial_audit_service = FinancialAuditService()
|
||||
|
||||
Reference in New Issue
Block a user