This commit is contained in:
Iliyan Angelov
2025-12-04 01:07:34 +02:00
parent 5fb50983a9
commit 3d634b4fce
92 changed files with 9678 additions and 221 deletions

View File

@@ -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')

View File

@@ -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

View File

@@ -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}

View File

@@ -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()