update
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
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
|
||||
@@ -20,7 +21,19 @@ 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 ..schemas.payment import CreatePaymentRequest, UpdatePaymentStatusRequest, CreateStripePaymentIntentRequest
|
||||
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'])
|
||||
@@ -186,6 +199,11 @@ async def create_payment(
|
||||
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
|
||||
@@ -193,19 +211,56 @@ async def create_payment(
|
||||
payment_type = payment_data.payment_type
|
||||
mark_as_paid = payment_data.mark_as_paid
|
||||
notes = payment_data.notes
|
||||
idempotency_key = payment_data.idempotency_key
|
||||
|
||||
booking = db.query(Booking).filter(Booking.id == booking_id).first()
|
||||
# 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')
|
||||
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)
|
||||
|
||||
# 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.commit()
|
||||
db.flush()
|
||||
|
||||
# Commit transaction
|
||||
transaction.commit()
|
||||
db.refresh(payment)
|
||||
|
||||
# Send payment receipt notification
|
||||
@@ -278,9 +333,21 @@ async def create_payment(
|
||||
|
||||
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:
|
||||
db.rollback()
|
||||
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,
|
||||
@@ -482,12 +549,11 @@ async def create_stripe_payment_intent(intent_data: CreateStripePaymentIntentReq
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.post('/stripe/confirm')
|
||||
async def confirm_stripe_payment(payment_data: dict, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||
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.get('payment_intent_id')
|
||||
booking_id = payment_data.get('booking_id')
|
||||
if not payment_intent_id:
|
||||
raise HTTPException(status_code=400, detail='payment_intent_id is required')
|
||||
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()
|
||||
@@ -549,7 +615,7 @@ async def stripe_webhook(request: Request, db: Session=Depends(get_db)):
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.post('/paypal/create-order')
|
||||
async def create_paypal_order(order_data: dict, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||
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)
|
||||
@@ -560,11 +626,9 @@ async def create_paypal_order(order_data: dict, current_user: User=Depends(get_c
|
||||
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.get('booking_id')
|
||||
amount = float(order_data.get('amount', 0))
|
||||
currency = order_data.get('currency', 'USD')
|
||||
if not booking_id or amount <= 0:
|
||||
raise HTTPException(status_code=400, detail='booking_id and amount are required')
|
||||
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
|
||||
@@ -603,11 +667,9 @@ async def create_paypal_order(order_data: dict, current_user: User=Depends(get_c
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.post('/paypal/cancel')
|
||||
async def cancel_paypal_payment(payment_data: dict, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||
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.get('booking_id')
|
||||
if not booking_id:
|
||||
raise HTTPException(status_code=400, detail='booking_id is required')
|
||||
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()
|
||||
@@ -627,12 +689,10 @@ async def cancel_paypal_payment(payment_data: dict, current_user: User=Depends(g
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.post('/paypal/capture')
|
||||
async def capture_paypal_payment(payment_data: dict, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||
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.get('order_id')
|
||||
booking_id = payment_data.get('booking_id')
|
||||
if not order_id:
|
||||
raise HTTPException(status_code=400, detail='order_id is required')
|
||||
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()
|
||||
@@ -673,7 +733,7 @@ async def capture_paypal_payment(payment_data: dict, current_user: User=Depends(
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.post('/borica/create-payment')
|
||||
async def create_borica_payment(payment_data: dict, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||
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)
|
||||
@@ -681,11 +741,9 @@ async def create_borica_payment(payment_data: dict, current_user: User=Depends(g
|
||||
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.get('booking_id')
|
||||
amount = float(payment_data.get('amount', 0))
|
||||
currency = payment_data.get('currency', 'BGN')
|
||||
if not booking_id or amount <= 0:
|
||||
raise HTTPException(status_code=400, detail='booking_id and amount are required')
|
||||
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
|
||||
@@ -705,7 +763,7 @@ async def create_borica_payment(payment_data: dict, current_user: User=Depends(g
|
||||
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 = payment_data.get('return_url', f'{client_url}/payment/borica/return')
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user