913 lines
57 KiB
Python
913 lines
57 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, status, Query, Request, Header
|
|
from sqlalchemy.orm import Session, joinedload, selectinload, load_only
|
|
from sqlalchemy.exc import IntegrityError
|
|
from typing import Optional
|
|
from datetime import datetime
|
|
import os
|
|
from ...shared.config.database import get_db
|
|
from ...shared.config.settings import settings
|
|
from ...shared.config.logging_config import get_logger
|
|
from ...security.middleware.auth import get_current_user, authorize_roles
|
|
from ...auth.models.user import User
|
|
from ..models.payment import Payment, PaymentMethod, PaymentType, PaymentStatus
|
|
from ...bookings.models.booking import Booking, BookingStatus
|
|
from ...shared.utils.role_helpers import can_access_all_payments
|
|
from ...shared.utils.currency_helpers import get_currency_symbol
|
|
from ...shared.utils.response_helpers import success_response
|
|
from ...shared.utils.mailer import send_email
|
|
from ...shared.utils.email_templates import payment_confirmation_email_template, booking_status_changed_email_template
|
|
from ..services.stripe_service import StripeService
|
|
from ..services.paypal_service import PayPalService
|
|
from ..services.borica_service import BoricaService
|
|
from ...loyalty.services.loyalty_service import LoyaltyService
|
|
from ...analytics.services.audit_service import audit_service
|
|
from ..services.financial_audit_service import financial_audit_service
|
|
from ..models.financial_audit_trail import FinancialActionType
|
|
from ..schemas.payment import (
|
|
CreatePaymentRequest,
|
|
UpdatePaymentStatusRequest,
|
|
CreateStripePaymentIntentRequest,
|
|
ConfirmStripePaymentRequest,
|
|
CreatePayPalOrderRequest,
|
|
CancelPayPalPaymentRequest,
|
|
CapturePayPalPaymentRequest,
|
|
CreateBoricaPaymentRequest,
|
|
ConfirmBoricaPaymentRequest
|
|
)
|
|
|
|
logger = get_logger(__name__)
|
|
router = APIRouter(prefix='/payments', tags=['payments'])
|
|
|
|
async def cancel_booking_on_payment_failure(booking: Booking, db: Session, reason: str='Payment failed or canceled'):
|
|
if booking.status == BookingStatus.cancelled:
|
|
return
|
|
from sqlalchemy.orm import selectinload
|
|
# Use load_only to exclude non-existent columns (rate_plan_id, group_booking_id)
|
|
booking = db.query(Booking).options(
|
|
load_only(
|
|
Booking.id, Booking.booking_number, Booking.user_id, Booking.room_id,
|
|
Booking.check_in_date, Booking.check_out_date, Booking.num_guests,
|
|
Booking.total_price, Booking.original_price, Booking.discount_amount,
|
|
Booking.promotion_code, Booking.status, Booking.deposit_paid,
|
|
Booking.requires_deposit, Booking.special_requests,
|
|
Booking.created_at, Booking.updated_at
|
|
),
|
|
selectinload(Booking.payments)
|
|
).filter(Booking.id == booking.id).first()
|
|
if booking.payments:
|
|
for payment in booking.payments:
|
|
if payment.payment_status == PaymentStatus.pending:
|
|
payment.payment_status = PaymentStatus.failed
|
|
existing_notes = payment.notes or ''
|
|
cancellation_note = f'\nPayment cancelled due to booking cancellation: {reason} on {datetime.utcnow().isoformat()}'
|
|
payment.notes = existing_notes + cancellation_note if existing_notes else cancellation_note.strip()
|
|
booking.status = BookingStatus.cancelled
|
|
db.commit()
|
|
db.refresh(booking)
|
|
try:
|
|
from ...system.models.system_settings import SystemSettings
|
|
client_url_setting = db.query(SystemSettings).filter(SystemSettings.key == 'client_url').first()
|
|
client_url = client_url_setting.value if client_url_setting and client_url_setting.value else settings.CLIENT_URL or os.getenv('CLIENT_URL', 'http://localhost:5173')
|
|
if booking.user:
|
|
email_html = booking_status_changed_email_template(booking_number=booking.booking_number, guest_name=booking.user.full_name if booking.user else 'Guest', status='cancelled', client_url=client_url)
|
|
await send_email(to=booking.user.email, subject=f'Booking Cancelled - {booking.booking_number}', html=email_html)
|
|
except Exception as e:
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.error(f'Failed to send cancellation email: {e}')
|
|
|
|
@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:
|
|
query = db.query(Payment)
|
|
if status_filter:
|
|
try:
|
|
query = query.filter(Payment.payment_status == PaymentStatus(status_filter))
|
|
except ValueError:
|
|
pass
|
|
if not can_access_all_payments(current_user, db):
|
|
query = query.join(Booking).filter(Booking.user_id == current_user.id)
|
|
total = query.count()
|
|
# Use load_only to exclude non-existent columns (rate_plan_id, group_booking_id)
|
|
query = query.options(
|
|
selectinload(Payment.booking).options(
|
|
load_only(
|
|
Booking.id, Booking.booking_number, Booking.user_id, Booking.room_id,
|
|
Booking.check_in_date, Booking.check_out_date, Booking.num_guests,
|
|
Booking.total_price, Booking.original_price, Booking.discount_amount,
|
|
Booking.promotion_code, Booking.status, Booking.deposit_paid,
|
|
Booking.requires_deposit, Booking.special_requests,
|
|
Booking.created_at, Booking.updated_at
|
|
),
|
|
joinedload(Booking.user)
|
|
)
|
|
)
|
|
offset = (page - 1) * limit
|
|
payments = query.order_by(Payment.created_at.desc()).offset(offset).limit(limit).all()
|
|
result = []
|
|
for payment in payments:
|
|
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}
|
|
if payment.booking.user:
|
|
payment_dict['booking']['user'] = {'id': payment.booking.user.id, 'name': payment.booking.user.full_name, 'full_name': payment.booking.user.full_name, 'email': payment.booking.user.email}
|
|
result.append(payment_dict)
|
|
return success_response(data={'payments': result, 'pagination': {'total': total, 'page': page, 'limit': limit, 'totalPages': (total + limit - 1) // limit}})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.error(f'Error fetching payments: {str(e)}', exc_info=True)
|
|
raise HTTPException(status_code=500, detail=f'Error fetching payments: {str(e)}')
|
|
|
|
@router.get('/booking/{booking_id}')
|
|
async def get_payments_by_booking_id(booking_id: int, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
|
try:
|
|
from ...shared.utils.role_helpers import is_admin
|
|
booking = db.query(Booking).filter(Booking.id == booking_id).first()
|
|
if not booking:
|
|
raise HTTPException(status_code=404, detail='Booking not found')
|
|
if not is_admin(current_user, db) and booking.user_id != current_user.id:
|
|
raise HTTPException(status_code=403, detail='Forbidden')
|
|
# Use load_only to exclude non-existent columns (rate_plan_id, group_booking_id)
|
|
payments = db.query(Payment).options(
|
|
joinedload(Payment.booking).options(
|
|
load_only(
|
|
Booking.id, Booking.booking_number, Booking.user_id, Booking.room_id,
|
|
Booking.check_in_date, Booking.check_out_date, Booking.num_guests,
|
|
Booking.total_price, Booking.original_price, Booking.discount_amount,
|
|
Booking.promotion_code, Booking.status, Booking.deposit_paid,
|
|
Booking.requires_deposit, Booking.special_requests,
|
|
Booking.created_at, Booking.updated_at
|
|
),
|
|
joinedload(Booking.user)
|
|
)
|
|
).filter(Payment.booking_id == booking_id).order_by(Payment.created_at.desc()).all()
|
|
result = []
|
|
for payment in payments:
|
|
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}
|
|
if payment.booking.user:
|
|
payment_dict['booking']['user'] = {'id': payment.booking.user.id, 'name': payment.booking.user.full_name, 'full_name': payment.booking.user.full_name, 'email': payment.booking.user.email}
|
|
result.append(payment_dict)
|
|
return success_response(data={'payments': result})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
db.rollback()
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.error(f'Error in get_payments_by_booking_id: {str(e)}', extra={'booking_id': booking_id}, exc_info=True)
|
|
raise HTTPException(status_code=500, detail='An error occurred while fetching payments')
|
|
|
|
@router.get('/{id}')
|
|
async def get_payment_by_id(id: int, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
|
try:
|
|
# 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 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}
|
|
return success_response(data={'payment': payment_dict})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
db.rollback()
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.error(f'Error in get_payment_by_id: {str(e)}', extra={'payment_id': id}, exc_info=True)
|
|
raise HTTPException(status_code=500, detail='An error occurred while fetching payment')
|
|
|
|
@router.post('/')
|
|
async def create_payment(
|
|
request: Request,
|
|
payment_data: CreatePaymentRequest,
|
|
current_user: User=Depends(get_current_user),
|
|
db: Session=Depends(get_db)
|
|
):
|
|
"""Create a payment with validated input using Pydantic schema."""
|
|
client_ip = request.client.host if request.client else None
|
|
user_agent = request.headers.get('User-Agent')
|
|
request_id = getattr(request.state, 'request_id', None)
|
|
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Start transaction
|
|
transaction = db.begin()
|
|
try:
|
|
booking_id = payment_data.booking_id
|
|
amount = payment_data.amount
|
|
payment_method = payment_data.payment_method
|
|
payment_type = payment_data.payment_type
|
|
mark_as_paid = payment_data.mark_as_paid
|
|
notes = payment_data.notes
|
|
idempotency_key = payment_data.idempotency_key
|
|
|
|
# Idempotency check: if idempotency_key provided, check for existing payment
|
|
if idempotency_key:
|
|
existing_payment = db.query(Payment).filter(
|
|
Payment.transaction_id == idempotency_key,
|
|
Payment.booking_id == booking_id,
|
|
Payment.amount == amount
|
|
).first()
|
|
if existing_payment:
|
|
transaction.rollback()
|
|
logger.info(f'Duplicate payment request detected with idempotency_key: {idempotency_key}')
|
|
return success_response(
|
|
data={'payment': existing_payment},
|
|
message='Payment already exists (idempotency check)'
|
|
)
|
|
|
|
# Lock booking row to prevent race conditions
|
|
booking = db.query(Booking).filter(Booking.id == booking_id).with_for_update().first()
|
|
if not booking:
|
|
transaction.rollback()
|
|
raise HTTPException(status_code=404, detail='Booking not found')
|
|
|
|
from ...shared.utils.role_helpers import is_admin
|
|
if not is_admin(current_user, db) and booking.user_id != current_user.id:
|
|
transaction.rollback()
|
|
raise HTTPException(status_code=403, detail='Forbidden')
|
|
|
|
# Use idempotency_key as transaction_id if provided
|
|
transaction_id = idempotency_key if idempotency_key else None
|
|
|
|
payment = Payment(
|
|
booking_id=booking_id,
|
|
amount=amount,
|
|
payment_method=PaymentMethod(payment_method),
|
|
payment_type=PaymentType(payment_type),
|
|
payment_status=PaymentStatus.pending,
|
|
payment_date=datetime.utcnow() if mark_as_paid else None,
|
|
notes=notes,
|
|
transaction_id=transaction_id
|
|
)
|
|
if mark_as_paid:
|
|
payment.payment_status = PaymentStatus.completed
|
|
payment.payment_date = datetime.utcnow()
|
|
|
|
db.add(payment)
|
|
db.flush()
|
|
|
|
# Commit transaction
|
|
transaction.commit()
|
|
db.refresh(payment)
|
|
|
|
# Send payment receipt notification
|
|
if payment.payment_status == PaymentStatus.completed:
|
|
try:
|
|
from ...notifications.services.notification_service import NotificationService
|
|
NotificationService.send_payment_receipt(db, payment)
|
|
except Exception as e:
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.warning(f'Failed to send payment receipt notification: {e}')
|
|
|
|
# Award loyalty points if payment completed and booking is confirmed
|
|
if payment.payment_status == PaymentStatus.completed and booking:
|
|
try:
|
|
db.refresh(booking)
|
|
if booking.status == BookingStatus.confirmed and booking.user:
|
|
# Check if booking already earned points
|
|
from ...loyalty.models.loyalty_point_transaction import LoyaltyPointTransaction, TransactionSource
|
|
existing_points = db.query(LoyaltyPointTransaction).filter(
|
|
LoyaltyPointTransaction.booking_id == booking.id,
|
|
LoyaltyPointTransaction.source == TransactionSource.booking
|
|
).first()
|
|
|
|
if not existing_points:
|
|
# Award points based on payment amount
|
|
total_paid = sum(float(p.amount) for p in booking.payments if p.payment_status == PaymentStatus.completed)
|
|
if total_paid > 0:
|
|
LoyaltyService.earn_points_from_booking(db, booking.user_id, booking, total_paid)
|
|
except Exception as loyalty_error:
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.error(f'Failed to award loyalty points: {loyalty_error}')
|
|
|
|
if payment.payment_status == PaymentStatus.completed and booking.user:
|
|
try:
|
|
from ...system.models.system_settings import SystemSettings
|
|
client_url_setting = db.query(SystemSettings).filter(SystemSettings.key == 'client_url').first()
|
|
client_url = client_url_setting.value if client_url_setting and client_url_setting.value else settings.CLIENT_URL or os.getenv('CLIENT_URL', 'http://localhost:5173')
|
|
currency_setting = db.query(SystemSettings).filter(SystemSettings.key == 'platform_currency').first()
|
|
currency = currency_setting.value if currency_setting and currency_setting.value else 'USD'
|
|
currency_symbol = get_currency_symbol(currency)
|
|
email_html = payment_confirmation_email_template(booking_number=booking.booking_number, guest_name=booking.user.full_name, amount=float(payment.amount), payment_method=payment.payment_method.value if isinstance(payment.payment_method, PaymentMethod) else str(payment.payment_method), transaction_id=payment.transaction_id, payment_type=payment.payment_type.value if payment.payment_type else None, total_price=float(booking.total_price), client_url=client_url, currency_symbol=currency_symbol)
|
|
await send_email(to=booking.user.email, subject=f'Payment Confirmed - {booking.booking_number}', html=email_html)
|
|
except Exception as e:
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.error(f'Failed to send payment confirmation email: {e}')
|
|
|
|
# Log payment transaction
|
|
await audit_service.log_action(
|
|
db=db,
|
|
action='payment_created',
|
|
resource_type='payment',
|
|
user_id=current_user.id,
|
|
resource_id=payment.id,
|
|
ip_address=client_ip,
|
|
user_agent=user_agent,
|
|
request_id=request_id,
|
|
details={
|
|
'booking_id': booking_id,
|
|
'amount': float(amount),
|
|
'payment_method': payment_method,
|
|
'payment_type': payment_type,
|
|
'payment_status': payment.payment_status.value if hasattr(payment.payment_status, 'value') else str(payment.payment_status),
|
|
'transaction_id': payment.transaction_id
|
|
},
|
|
status='success'
|
|
)
|
|
|
|
return success_response(data={'payment': payment}, message='Payment created successfully')
|
|
except HTTPException:
|
|
if 'transaction' in locals():
|
|
transaction.rollback()
|
|
raise
|
|
except IntegrityError as e:
|
|
if 'transaction' in locals():
|
|
transaction.rollback()
|
|
logger.error(f'Database integrity error during payment creation: {str(e)}')
|
|
# Check if it's a duplicate payment error
|
|
if 'duplicate' in str(e).lower() or 'unique' in str(e).lower():
|
|
raise HTTPException(status_code=409, detail='Duplicate payment detected. Please check if payment already exists.')
|
|
raise HTTPException(status_code=409, detail='Payment conflict detected. Please try again.')
|
|
except Exception as e:
|
|
if 'transaction' in locals():
|
|
transaction.rollback()
|
|
logger.error(f'Error creating payment: {str(e)}', exc_info=True)
|
|
# Log failed payment creation
|
|
await audit_service.log_action(
|
|
db=db,
|
|
action='payment_creation_failed',
|
|
resource_type='payment',
|
|
user_id=current_user.id if current_user else None,
|
|
ip_address=client_ip,
|
|
user_agent=user_agent,
|
|
request_id=request_id,
|
|
details={'booking_id': payment_data.booking_id, 'amount': payment_data.amount},
|
|
status='failed',
|
|
error_message=str(e)
|
|
)
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@router.put('/{id}/status', dependencies=[Depends(authorize_roles('admin', 'staff', 'accountant'))])
|
|
async def update_payment_status(
|
|
request: Request,
|
|
id: int,
|
|
status_data: UpdatePaymentStatusRequest,
|
|
current_user: User=Depends(authorize_roles('admin', 'staff', 'accountant')),
|
|
db: Session=Depends(get_db)
|
|
):
|
|
"""Update payment status with validated input using Pydantic schema."""
|
|
client_ip = request.client.host if request.client else None
|
|
user_agent = request.headers.get('User-Agent')
|
|
request_id = getattr(request.state, 'request_id', None)
|
|
|
|
try:
|
|
payment = db.query(Payment).filter(Payment.id == id).first()
|
|
if not payment:
|
|
raise HTTPException(status_code=404, detail='Payment not found')
|
|
status_value = status_data.status
|
|
notes = status_data.notes
|
|
old_status = payment.payment_status
|
|
if status_value:
|
|
try:
|
|
new_status = PaymentStatus(status_value)
|
|
payment.payment_status = new_status
|
|
# Only cancel booking if it's a full refund or all payments are failed
|
|
if new_status in [PaymentStatus.failed, PaymentStatus.refunded]:
|
|
# Use load_only to exclude non-existent columns (rate_plan_id, group_booking_id)
|
|
booking = db.query(Booking).options(
|
|
load_only(
|
|
Booking.id, Booking.booking_number, Booking.user_id, Booking.room_id,
|
|
Booking.check_in_date, Booking.check_out_date, Booking.num_guests,
|
|
Booking.total_price, Booking.original_price, Booking.discount_amount,
|
|
Booking.promotion_code, Booking.status, Booking.deposit_paid,
|
|
Booking.requires_deposit, Booking.special_requests,
|
|
Booking.created_at, Booking.updated_at
|
|
),
|
|
selectinload(Booking.payments)
|
|
).filter(Booking.id == payment.booking_id).first()
|
|
if booking and booking.status != BookingStatus.cancelled:
|
|
# Check if this is a full refund or if all payments are failed
|
|
total_paid = sum(float(p.amount) for p in booking.payments if p.payment_status == PaymentStatus.completed)
|
|
total_price = float(booking.total_price) if booking.total_price else 0.0
|
|
all_payments_failed = all(p.payment_status in [PaymentStatus.failed, PaymentStatus.refunded] for p in booking.payments)
|
|
is_full_refund = new_status == PaymentStatus.refunded and float(payment.amount) >= total_price
|
|
|
|
# Only cancel if it's a full refund or all payments failed
|
|
if is_full_refund or (new_status == PaymentStatus.failed and all_payments_failed):
|
|
await cancel_booking_on_payment_failure(booking, db, reason=f'Payment {new_status.value}')
|
|
except ValueError:
|
|
raise HTTPException(status_code=400, detail='Invalid payment status')
|
|
|
|
# Update notes if provided
|
|
if notes:
|
|
existing_notes = payment.notes or ''
|
|
payment.notes = f'{existing_notes}\n{notes}'.strip() if existing_notes else notes
|
|
db.commit()
|
|
db.refresh(payment)
|
|
|
|
# Log payment status update (admin action)
|
|
await audit_service.log_action(
|
|
db=db,
|
|
action='payment_status_updated',
|
|
resource_type='payment',
|
|
user_id=current_user.id,
|
|
resource_id=payment.id,
|
|
ip_address=client_ip,
|
|
user_agent=user_agent,
|
|
request_id=request_id,
|
|
details={
|
|
'payment_id': id,
|
|
'old_status': old_status.value if hasattr(old_status, 'value') else str(old_status),
|
|
'new_status': payment.payment_status.value if hasattr(payment.payment_status, 'value') else str(payment.payment_status),
|
|
'booking_id': payment.booking_id,
|
|
'amount': float(payment.amount) if payment.amount else 0.0,
|
|
'notes': notes
|
|
},
|
|
status='success'
|
|
)
|
|
|
|
if payment.payment_status == PaymentStatus.completed and old_status != PaymentStatus.completed:
|
|
# Send payment receipt notification
|
|
try:
|
|
from ...notifications.services.notification_service import NotificationService
|
|
NotificationService.send_payment_receipt(db, payment)
|
|
except Exception as e:
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.warning(f'Failed to send payment receipt notification: {e}')
|
|
|
|
try:
|
|
from ...system.models.system_settings import SystemSettings
|
|
client_url_setting = db.query(SystemSettings).filter(SystemSettings.key == 'client_url').first()
|
|
client_url = client_url_setting.value if client_url_setting and client_url_setting.value else settings.CLIENT_URL or os.getenv('CLIENT_URL', 'http://localhost:5173')
|
|
currency_setting = db.query(SystemSettings).filter(SystemSettings.key == 'platform_currency').first()
|
|
currency = currency_setting.value if currency_setting and currency_setting.value else 'USD'
|
|
currency_symbol = get_currency_symbol(currency)
|
|
payment = db.query(Payment).filter(Payment.id == id).first()
|
|
if payment.booking and payment.booking.user:
|
|
client_url_setting = db.query(SystemSettings).filter(SystemSettings.key == 'client_url').first()
|
|
client_url = client_url_setting.value if client_url_setting and client_url_setting.value else settings.CLIENT_URL or os.getenv('CLIENT_URL', 'http://localhost:5173')
|
|
currency_setting = db.query(SystemSettings).filter(SystemSettings.key == 'platform_currency').first()
|
|
currency = currency_setting.value if currency_setting and currency_setting.value else 'USD'
|
|
currency_symbol = get_currency_symbol(currency)
|
|
email_html = payment_confirmation_email_template(booking_number=payment.booking.booking_number, guest_name=payment.booking.user.full_name, amount=float(payment.amount), payment_method=payment.payment_method.value if isinstance(payment.payment_method, PaymentMethod) else str(payment.payment_method), transaction_id=payment.transaction_id, client_url=client_url, currency_symbol=currency_symbol)
|
|
await send_email(to=payment.booking.user.email, subject=f'Payment Confirmed - {payment.booking.booking_number}', html=email_html)
|
|
if payment.payment_type == PaymentType.deposit and payment.booking:
|
|
payment.booking.deposit_paid = True
|
|
if payment.booking.status in [BookingStatus.pending, BookingStatus.cancelled]:
|
|
payment.booking.status = BookingStatus.confirmed
|
|
db.commit()
|
|
elif payment.payment_type == PaymentType.full and payment.booking:
|
|
total_paid = sum((float(p.amount) for p in payment.booking.payments if p.payment_status == PaymentStatus.completed))
|
|
if total_paid >= float(payment.booking.total_price):
|
|
if payment.booking.status in [BookingStatus.pending, BookingStatus.cancelled]:
|
|
payment.booking.status = BookingStatus.confirmed
|
|
db.commit()
|
|
except Exception as e:
|
|
logger.error(f'Failed to send payment confirmation email: {str(e)}', exc_info=True, extra={'payment_id': payment.id if hasattr(payment, 'id') else None})
|
|
return success_response(data={'payment': payment}, message='Payment status updated successfully')
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
db.rollback()
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@router.post('/stripe/create-intent')
|
|
async def create_stripe_payment_intent(intent_data: CreateStripePaymentIntentRequest, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
|
try:
|
|
from ..services.stripe_service import get_stripe_secret_key
|
|
secret_key = get_stripe_secret_key(db)
|
|
if not secret_key:
|
|
secret_key = settings.STRIPE_SECRET_KEY
|
|
if not secret_key:
|
|
raise HTTPException(status_code=500, detail='Stripe is not configured. Please configure Stripe settings in Admin Panel or set STRIPE_SECRET_KEY environment variable.')
|
|
booking_id = intent_data.booking_id
|
|
amount = intent_data.amount
|
|
currency = intent_data.currency or 'usd'
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.info(f'Creating Stripe payment intent - Booking ID: {booking_id}, Amount: ${amount:,.2f}, Currency: {currency}')
|
|
if amount > 999999.99:
|
|
logger.error(f"Amount ${amount:,.2f} exceeds Stripe's maximum of $999,999.99")
|
|
raise HTTPException(status_code=400, detail=f"Amount ${amount:,.2f} exceeds Stripe's maximum of $999,999.99. Please contact support for large payments.")
|
|
booking = db.query(Booking).filter(Booking.id == booking_id).first()
|
|
if not booking:
|
|
raise HTTPException(status_code=404, detail='Booking not found')
|
|
from ...shared.utils.role_helpers import is_admin
|
|
if not is_admin(current_user, db) and booking.user_id != current_user.id:
|
|
raise HTTPException(status_code=403, detail='Forbidden')
|
|
if booking.requires_deposit and (not booking.deposit_paid):
|
|
deposit_payment = db.query(Payment).filter(Payment.booking_id == booking_id, Payment.payment_type == PaymentType.deposit, Payment.payment_status == PaymentStatus.pending).order_by(Payment.created_at.desc()).first()
|
|
if deposit_payment:
|
|
expected_deposit_amount = float(deposit_payment.amount)
|
|
if abs(amount - expected_deposit_amount) > 0.01:
|
|
logger.warning(f'Amount mismatch for deposit payment: Requested ${amount:,.2f}, Expected deposit ${expected_deposit_amount:,.2f}, Booking total ${float(booking.total_price):,.2f}')
|
|
raise HTTPException(status_code=400, detail=f'For pay-on-arrival bookings, only the deposit amount (${expected_deposit_amount:,.2f}) should be charged, not the full booking amount (${float(booking.total_price):,.2f}).')
|
|
intent = StripeService.create_payment_intent(amount=amount, currency=currency, metadata={'booking_id': str(booking_id), 'booking_number': booking.booking_number, 'user_id': str(current_user.id)}, db=db)
|
|
from ..services.stripe_service import get_stripe_publishable_key
|
|
publishable_key = get_stripe_publishable_key(db)
|
|
if not publishable_key:
|
|
publishable_key = settings.STRIPE_PUBLISHABLE_KEY
|
|
if not publishable_key:
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.warning('Stripe publishable key is not configured')
|
|
raise HTTPException(status_code=500, detail='Stripe publishable key is not configured. Please configure it in Admin Panel (Settings > Stripe Settings) or set STRIPE_PUBLISHABLE_KEY environment variable.')
|
|
if not intent.get('client_secret'):
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.error('Payment intent created but client_secret is missing')
|
|
raise HTTPException(status_code=500, detail='Failed to create payment intent. Client secret is missing.')
|
|
return success_response(data={'client_secret': intent['client_secret'], 'payment_intent_id': intent['id'], 'publishable_key': publishable_key}, message='Payment intent created successfully')
|
|
except HTTPException:
|
|
raise
|
|
except ValueError as e:
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.error(f'Payment intent creation error: {str(e)}')
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.error(f'Unexpected error creating payment intent: {str(e)}', exc_info=True)
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@router.post('/stripe/confirm')
|
|
async def confirm_stripe_payment(payment_data: ConfirmStripePaymentRequest, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
|
"""Confirm a Stripe payment with validated input using Pydantic schema."""
|
|
try:
|
|
payment_intent_id = payment_data.payment_intent_id
|
|
booking_id = payment_data.booking_id
|
|
payment = await StripeService.confirm_payment(payment_intent_id=payment_intent_id, db=db, booking_id=booking_id)
|
|
try:
|
|
db.commit()
|
|
except Exception:
|
|
pass
|
|
booking = db.query(Booking).filter(Booking.id == payment['booking_id']).first()
|
|
if booking:
|
|
db.refresh(booking)
|
|
if booking and booking.user:
|
|
try:
|
|
from ...system.models.system_settings import SystemSettings
|
|
client_url_setting = db.query(SystemSettings).filter(SystemSettings.key == 'client_url').first()
|
|
client_url = client_url_setting.value if client_url_setting and client_url_setting.value else settings.CLIENT_URL or os.getenv('CLIENT_URL', 'http://localhost:5173')
|
|
currency_setting = db.query(SystemSettings).filter(SystemSettings.key == 'platform_currency').first()
|
|
currency = currency_setting.value if currency_setting and currency_setting.value else 'USD'
|
|
currency_symbol = get_currency_symbol(currency)
|
|
email_html = payment_confirmation_email_template(booking_number=booking.booking_number, guest_name=booking.user.full_name, amount=payment['amount'], payment_method='stripe', transaction_id=payment['transaction_id'], payment_type=payment.get('payment_type'), total_price=float(booking.total_price), client_url=client_url, currency_symbol=currency_symbol)
|
|
await send_email(to=booking.user.email, subject=f'Payment Confirmed - {booking.booking_number}', html=email_html)
|
|
except Exception as e:
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.warning(f'Failed to send payment confirmation email: {e}')
|
|
return success_response(data={'payment': payment, 'booking': {'id': booking.id if booking else None, 'booking_number': booking.booking_number if booking else None, 'status': booking.status.value if booking else None}}, message='Payment confirmed successfully')
|
|
except HTTPException:
|
|
db.rollback()
|
|
raise
|
|
except ValueError as e:
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.error(f'Payment confirmation error: {str(e)}')
|
|
db.rollback()
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.error(f'Unexpected error confirming payment: {str(e)}', exc_info=True)
|
|
db.rollback()
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@router.post('/stripe/webhook')
|
|
async def stripe_webhook(request: Request, db: Session=Depends(get_db)):
|
|
try:
|
|
from ..services.stripe_service import get_stripe_webhook_secret
|
|
webhook_secret = get_stripe_webhook_secret(db)
|
|
if not webhook_secret:
|
|
webhook_secret = settings.STRIPE_WEBHOOK_SECRET
|
|
if not webhook_secret:
|
|
raise HTTPException(status_code=503, detail={'status': 'error', 'message': 'Stripe webhook secret is not configured. Please configure it in Admin Panel (Settings > Stripe Settings) or set STRIPE_WEBHOOK_SECRET environment variable.'})
|
|
payload = await request.body()
|
|
signature = request.headers.get('stripe-signature')
|
|
if not signature:
|
|
raise HTTPException(status_code=400, detail='Missing stripe-signature header')
|
|
result = await StripeService.handle_webhook(payload=payload, signature=signature, db=db)
|
|
return success_response(data=result)
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
db.rollback()
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@router.post('/paypal/create-order')
|
|
async def create_paypal_order(order_data: CreatePayPalOrderRequest, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
|
try:
|
|
from ..services.paypal_service import get_paypal_client_id, get_paypal_client_secret
|
|
client_id = get_paypal_client_id(db)
|
|
if not client_id:
|
|
client_id = settings.PAYPAL_CLIENT_ID
|
|
client_secret = get_paypal_client_secret(db)
|
|
if not client_secret:
|
|
client_secret = settings.PAYPAL_CLIENT_SECRET
|
|
if not client_id or not client_secret:
|
|
raise HTTPException(status_code=500, detail='PayPal is not configured. Please configure PayPal settings in Admin Panel or set PAYPAL_CLIENT_ID and PAYPAL_CLIENT_SECRET environment variables.')
|
|
booking_id = order_data.booking_id
|
|
amount = float(order_data.amount)
|
|
currency = order_data.currency or 'USD'
|
|
if amount > 100000:
|
|
raise HTTPException(status_code=400, detail=f"Amount ${amount:,.2f} exceeds PayPal's maximum of $100,000. Please contact support for large payments.")
|
|
from ...shared.utils.role_helpers import is_admin
|
|
booking = db.query(Booking).filter(Booking.id == booking_id).first()
|
|
if not booking:
|
|
raise HTTPException(status_code=404, detail='Booking not found')
|
|
if not is_admin(current_user, db) and booking.user_id != current_user.id:
|
|
raise HTTPException(status_code=403, detail='Forbidden')
|
|
if booking.requires_deposit and (not booking.deposit_paid):
|
|
deposit_payment = db.query(Payment).filter(Payment.booking_id == booking_id, Payment.payment_type == PaymentType.deposit, Payment.payment_status == PaymentStatus.pending).order_by(Payment.created_at.desc()).first()
|
|
if deposit_payment:
|
|
expected_deposit_amount = float(deposit_payment.amount)
|
|
if abs(amount - expected_deposit_amount) > 0.01:
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.warning(f'Amount mismatch for deposit payment: Requested ${amount:,.2f}, Expected deposit ${expected_deposit_amount:,.2f}, Booking total ${float(booking.total_price):,.2f}')
|
|
raise HTTPException(status_code=400, detail=f'For pay-on-arrival bookings, only the deposit amount (${expected_deposit_amount:,.2f}) should be charged, not the full booking amount (${float(booking.total_price):,.2f}).')
|
|
client_url = settings.CLIENT_URL or os.getenv('CLIENT_URL', 'http://localhost:5173')
|
|
return_url = order_data.get('return_url', f'{client_url}/payment/paypal/return')
|
|
cancel_url = order_data.get('cancel_url', f'{client_url}/payment/paypal/cancel')
|
|
order = PayPalService.create_order(amount=amount, currency=currency, metadata={'booking_id': str(booking_id), 'booking_number': booking.booking_number, 'user_id': str(current_user.id), 'description': f'Hotel Booking Payment - {booking.booking_number}', 'return_url': return_url, 'cancel_url': cancel_url}, db=db)
|
|
if not order.get('approval_url'):
|
|
raise HTTPException(status_code=500, detail='Failed to create PayPal order. Approval URL is missing.')
|
|
return success_response(data={'order_id': order['id'], 'approval_url': order['approval_url'], 'status': order['status']}, message='PayPal order created successfully')
|
|
except HTTPException:
|
|
raise
|
|
except ValueError as e:
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.error(f'PayPal order creation error: {str(e)}')
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.error(f'Unexpected error creating PayPal order: {str(e)}', exc_info=True)
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@router.post('/paypal/cancel')
|
|
async def cancel_paypal_payment(payment_data: CancelPayPalPaymentRequest, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
|
try:
|
|
booking_id = payment_data.booking_id
|
|
payment = db.query(Payment).filter(Payment.booking_id == booking_id, Payment.payment_method == PaymentMethod.paypal, Payment.payment_status == PaymentStatus.pending).order_by(Payment.created_at.desc()).first()
|
|
if not payment:
|
|
payment = db.query(Payment).filter(Payment.booking_id == booking_id, Payment.payment_type == PaymentType.deposit, Payment.payment_status == PaymentStatus.pending).order_by(Payment.created_at.desc()).first()
|
|
if payment:
|
|
payment.payment_status = PaymentStatus.failed
|
|
db.commit()
|
|
db.refresh(payment)
|
|
booking = db.query(Booking).filter(Booking.id == booking_id).first()
|
|
if booking and booking.status != BookingStatus.cancelled:
|
|
await cancel_booking_on_payment_failure(booking, db, reason='PayPal payment canceled by user')
|
|
return success_response(message='Payment canceled and booking cancelled')
|
|
except HTTPException:
|
|
db.rollback()
|
|
raise
|
|
except Exception as e:
|
|
db.rollback()
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@router.post('/paypal/capture')
|
|
async def capture_paypal_payment(payment_data: CapturePayPalPaymentRequest, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
|
try:
|
|
order_id = payment_data.order_id
|
|
booking_id = payment_data.booking_id
|
|
payment = await PayPalService.confirm_payment(order_id=order_id, db=db, booking_id=booking_id)
|
|
try:
|
|
db.commit()
|
|
except Exception:
|
|
pass
|
|
booking = db.query(Booking).filter(Booking.id == payment['booking_id']).first()
|
|
if booking:
|
|
db.refresh(booking)
|
|
if booking and booking.user:
|
|
try:
|
|
from ...system.models.system_settings import SystemSettings
|
|
client_url_setting = db.query(SystemSettings).filter(SystemSettings.key == 'client_url').first()
|
|
client_url = client_url_setting.value if client_url_setting and client_url_setting.value else settings.CLIENT_URL or os.getenv('CLIENT_URL', 'http://localhost:5173')
|
|
currency_setting = db.query(SystemSettings).filter(SystemSettings.key == 'platform_currency').first()
|
|
currency = currency_setting.value if currency_setting and currency_setting.value else 'USD'
|
|
currency_symbol = get_currency_symbol(currency)
|
|
email_html = payment_confirmation_email_template(booking_number=booking.booking_number, guest_name=booking.user.full_name, amount=payment['amount'], payment_method='paypal', transaction_id=payment['transaction_id'], payment_type=payment.get('payment_type'), total_price=float(booking.total_price), client_url=client_url, currency_symbol=currency_symbol)
|
|
await send_email(to=booking.user.email, subject=f'Payment Confirmed - {booking.booking_number}', html=email_html)
|
|
except Exception as e:
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.warning(f'Failed to send payment confirmation email: {e}')
|
|
return success_response(data={'payment': payment, 'booking': {'id': booking.id if booking else None, 'booking_number': booking.booking_number if booking else None, 'status': booking.status.value if booking else None}}, message='Payment confirmed successfully')
|
|
except HTTPException:
|
|
db.rollback()
|
|
raise
|
|
except ValueError as e:
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.error(f'PayPal payment confirmation error: {str(e)}')
|
|
db.rollback()
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.error(f'Unexpected error confirming PayPal payment: {str(e)}', exc_info=True)
|
|
db.rollback()
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@router.post('/borica/create-payment')
|
|
async def create_borica_payment(payment_data: CreateBoricaPaymentRequest, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
|
try:
|
|
from ..services.borica_service import get_borica_terminal_id, get_borica_merchant_id
|
|
terminal_id = get_borica_terminal_id(db)
|
|
merchant_id = get_borica_merchant_id(db)
|
|
if not terminal_id or not merchant_id:
|
|
if not settings.BORICA_TERMINAL_ID or not settings.BORICA_MERCHANT_ID:
|
|
raise HTTPException(status_code=500, detail='Borica is not configured. Please configure Borica settings in Admin Panel or set BORICA_TERMINAL_ID and BORICA_MERCHANT_ID environment variables.')
|
|
booking_id = payment_data.booking_id
|
|
amount = float(payment_data.amount)
|
|
currency = payment_data.currency or 'BGN'
|
|
if amount > 100000:
|
|
raise HTTPException(status_code=400, detail=f"Amount {amount:,.2f} exceeds maximum of 100,000. Please contact support for large payments.")
|
|
from ...shared.utils.role_helpers import is_admin
|
|
booking = db.query(Booking).filter(Booking.id == booking_id).first()
|
|
if not booking:
|
|
raise HTTPException(status_code=404, detail='Booking not found')
|
|
if not is_admin(current_user, db) and booking.user_id != current_user.id:
|
|
raise HTTPException(status_code=403, detail='Forbidden')
|
|
if booking.requires_deposit and (not booking.deposit_paid):
|
|
deposit_payment = db.query(Payment).filter(Payment.booking_id == booking_id, Payment.payment_type == PaymentType.deposit, Payment.payment_status == PaymentStatus.pending).order_by(Payment.created_at.desc()).first()
|
|
if deposit_payment:
|
|
expected_deposit_amount = float(deposit_payment.amount)
|
|
if abs(amount - expected_deposit_amount) > 0.01:
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.warning(f'Amount mismatch for deposit payment: Requested {amount:,.2f}, Expected deposit {expected_deposit_amount:,.2f}, Booking total {float(booking.total_price):,.2f}')
|
|
raise HTTPException(status_code=400, detail=f'For pay-on-arrival bookings, only the deposit amount ({expected_deposit_amount:,.2f}) should be charged, not the full booking amount ({float(booking.total_price):,.2f}).')
|
|
transaction_id = BoricaService.generate_transaction_id(booking_id)
|
|
client_url = settings.CLIENT_URL or os.getenv('CLIENT_URL', 'http://localhost:5173')
|
|
return_url = f'{client_url}/payment/borica/return'
|
|
description = f'Hotel Booking Payment - {booking.booking_number}'
|
|
payment_request = BoricaService.create_payment_request(amount=amount, currency=currency, order_id=transaction_id, description=description, return_url=return_url, db=db)
|
|
payment_type = PaymentType.full
|
|
if booking.requires_deposit and (not booking.deposit_paid):
|
|
payment_type = PaymentType.deposit
|
|
payment = Payment(booking_id=booking_id, amount=amount, payment_method=PaymentMethod.borica, payment_type=payment_type, payment_status=PaymentStatus.pending, transaction_id=transaction_id, notes=f'Borica payment initiated - Order: {transaction_id}')
|
|
db.add(payment)
|
|
db.commit()
|
|
db.refresh(payment)
|
|
return success_response(data={'payment_request': payment_request, 'payment_id': payment.id, 'transaction_id': transaction_id}, message='Borica payment request created successfully')
|
|
except HTTPException:
|
|
raise
|
|
except ValueError as e:
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.error(f'Borica payment creation error: {str(e)}')
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.error(f'Unexpected error creating Borica payment: {str(e)}', exc_info=True)
|
|
db.rollback()
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@router.post('/borica/callback')
|
|
async def borica_callback(request: Request, db: Session=Depends(get_db)):
|
|
"""
|
|
Handle Borica payment callback (POST from Borica gateway).
|
|
Borica sends POST data with payment response.
|
|
"""
|
|
try:
|
|
form_data = await request.form()
|
|
response_data = dict(form_data)
|
|
|
|
# Also try to get from JSON if available
|
|
try:
|
|
json_data = await request.json()
|
|
response_data.update(json_data)
|
|
except:
|
|
pass
|
|
|
|
payment = await BoricaService.confirm_payment(response_data=response_data, db=db)
|
|
try:
|
|
db.commit()
|
|
except Exception:
|
|
pass
|
|
|
|
booking = db.query(Booking).filter(Booking.id == payment['booking_id']).first()
|
|
if booking:
|
|
db.refresh(booking)
|
|
|
|
if booking and booking.user:
|
|
try:
|
|
from ...system.models.system_settings import SystemSettings
|
|
client_url_setting = db.query(SystemSettings).filter(SystemSettings.key == 'client_url').first()
|
|
client_url = client_url_setting.value if client_url_setting and client_url_setting.value else settings.CLIENT_URL or os.getenv('CLIENT_URL', 'http://localhost:5173')
|
|
currency_setting = db.query(SystemSettings).filter(SystemSettings.key == 'platform_currency').first()
|
|
currency = currency_setting.value if currency_setting and currency_setting.value else 'USD'
|
|
currency_symbol = get_currency_symbol(currency)
|
|
email_html = payment_confirmation_email_template(booking_number=booking.booking_number, guest_name=booking.user.full_name, amount=payment['amount'], payment_method='borica', transaction_id=payment['transaction_id'], payment_type=payment.get('payment_type'), total_price=float(booking.total_price), client_url=client_url, currency_symbol=currency_symbol)
|
|
await send_email(to=booking.user.email, subject=f'Payment Confirmed - {booking.booking_number}', html=email_html)
|
|
except Exception as e:
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.warning(f'Failed to send payment confirmation email: {e}')
|
|
|
|
# Redirect to return URL with success status
|
|
return_url = response_data.get('BACKREF', '')
|
|
if return_url:
|
|
from fastapi.responses import RedirectResponse
|
|
return RedirectResponse(url=f"{return_url}?status=success&order={response_data.get('ORDER', '')}&bookingId={payment['booking_id']}")
|
|
|
|
return success_response(data={'payment': payment, 'booking': {'id': booking.id if booking else None, 'booking_number': booking.booking_number if booking else None, 'status': booking.status.value if booking else None}}, message='Payment confirmed successfully')
|
|
except HTTPException:
|
|
db.rollback()
|
|
raise
|
|
except ValueError as e:
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.error(f'Borica payment callback error: {str(e)}')
|
|
db.rollback()
|
|
# Redirect to return URL with error status
|
|
return_url = dict(await request.form()).get('BACKREF', '') if hasattr(request, 'form') else ''
|
|
if return_url:
|
|
from fastapi.responses import RedirectResponse
|
|
return RedirectResponse(url=f"{return_url}?status=error&error={str(e)}")
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.error(f'Unexpected error in Borica callback: {str(e)}', exc_info=True)
|
|
db.rollback()
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@router.post('/borica/confirm')
|
|
async def confirm_borica_payment(response_data: dict, db: Session=Depends(get_db)):
|
|
try:
|
|
payment = await BoricaService.confirm_payment(response_data=response_data, db=db)
|
|
try:
|
|
db.commit()
|
|
except Exception:
|
|
pass
|
|
booking = db.query(Booking).filter(Booking.id == payment['booking_id']).first()
|
|
if booking:
|
|
db.refresh(booking)
|
|
if booking and booking.user:
|
|
try:
|
|
from ...system.models.system_settings import SystemSettings
|
|
client_url_setting = db.query(SystemSettings).filter(SystemSettings.key == 'client_url').first()
|
|
client_url = client_url_setting.value if client_url_setting and client_url_setting.value else settings.CLIENT_URL or os.getenv('CLIENT_URL', 'http://localhost:5173')
|
|
currency_setting = db.query(SystemSettings).filter(SystemSettings.key == 'platform_currency').first()
|
|
currency = currency_setting.value if currency_setting and currency_setting.value else 'USD'
|
|
currency_symbol = get_currency_symbol(currency)
|
|
email_html = payment_confirmation_email_template(booking_number=booking.booking_number, guest_name=booking.user.full_name, amount=payment['amount'], payment_method='borica', transaction_id=payment['transaction_id'], payment_type=payment.get('payment_type'), total_price=float(booking.total_price), client_url=client_url, currency_symbol=currency_symbol)
|
|
await send_email(to=booking.user.email, subject=f'Payment Confirmed - {booking.booking_number}', html=email_html)
|
|
except Exception as e:
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.warning(f'Failed to send payment confirmation email: {e}')
|
|
return success_response(data={'payment': payment, 'booking': {'id': booking.id if booking else None, 'booking_number': booking.booking_number if booking else None, 'status': booking.status.value if booking else None}}, message='Payment confirmed successfully')
|
|
except HTTPException:
|
|
db.rollback()
|
|
raise
|
|
except ValueError as e:
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.error(f'Borica payment confirmation error: {str(e)}')
|
|
db.rollback()
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.error(f'Unexpected error confirming Borica payment: {str(e)}', exc_info=True)
|
|
db.rollback()
|
|
raise HTTPException(status_code=500, detail=str(e)) |