This commit is contained in:
Iliyan Angelov
2025-11-20 02:18:52 +02:00
parent 34b4c969d4
commit 44e11520c5
55 changed files with 4741 additions and 876 deletions

View File

@@ -1,6 +1,7 @@
"""
Stripe payment service for processing card payments
"""
import logging
import stripe
from typing import Optional, Dict, Any
from ..config.settings import settings
@@ -10,6 +11,8 @@ from ..models.system_settings import SystemSettings
from sqlalchemy.orm import Session
from datetime import datetime
logger = logging.getLogger(__name__)
def get_stripe_secret_key(db: Session) -> Optional[str]:
"""Get Stripe secret key from database or environment variable"""
@@ -183,7 +186,7 @@ class StripeService:
raise ValueError(f"Stripe error: {str(e)}")
@staticmethod
def confirm_payment(
async def confirm_payment(
payment_intent_id: str,
db: Session,
booking_id: Optional[int] = None
@@ -230,6 +233,15 @@ class StripeService:
Payment.payment_method == PaymentMethod.stripe
).first()
# If not found, try to find pending deposit payment (for cash bookings with deposit)
# This allows updating the payment_method from the default to stripe
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()
amount = intent_data["amount"]
if payment:
@@ -240,6 +252,7 @@ class StripeService:
payment.payment_date = datetime.utcnow()
# If processing, keep as pending (will be updated by webhook)
payment.amount = amount
payment.payment_method = PaymentMethod.stripe # Update payment method to Stripe
else:
# Create new payment record
payment_type = PaymentType.full
@@ -271,25 +284,148 @@ class StripeService:
# Refresh booking to get updated payments relationship
db.refresh(booking)
# Calculate total paid from all completed payments (now includes current payment)
# This needs to be calculated before the if/elif blocks
total_paid = sum(
float(p.amount) for p in booking.payments
if p.payment_status == PaymentStatus.completed
)
# Update invoice status based on payment
from ..models.invoice import Invoice, InvoiceStatus
from ..services.invoice_service import InvoiceService
# Find invoices for this booking and update their status
invoices = db.query(Invoice).filter(Invoice.booking_id == booking_id).all()
for invoice in invoices:
# Update invoice amount_paid and balance_due
invoice.amount_paid = total_paid
invoice.balance_due = float(invoice.total_amount) - total_paid
# Update invoice status
if invoice.balance_due <= 0:
invoice.status = InvoiceStatus.paid
invoice.paid_date = datetime.utcnow()
elif invoice.amount_paid > 0:
invoice.status = InvoiceStatus.sent
booking_was_confirmed = False
should_send_email = False
if payment.payment_type == PaymentType.deposit:
# Mark deposit as paid and confirm booking
booking.deposit_paid = True
if booking.status == BookingStatus.pending:
# Restore cancelled bookings or confirm pending bookings
if booking.status in [BookingStatus.pending, BookingStatus.cancelled]:
booking.status = BookingStatus.confirmed
booking_was_confirmed = True
should_send_email = True
elif booking.status == BookingStatus.confirmed:
# Booking already confirmed, but deposit was just paid
should_send_email = True
elif payment.payment_type == PaymentType.full:
# Calculate total paid from all completed payments (now includes current payment)
total_paid = sum(
float(p.amount) for p in booking.payments
if p.payment_status == PaymentStatus.completed
)
# Confirm booking if:
# 1. Total paid (all payments) covers the booking price, OR
# 2. This single payment covers the entire booking amount
# Also restore cancelled bookings when payment succeeds
if total_paid >= float(booking.total_price) or float(payment.amount) >= float(booking.total_price):
booking.status = BookingStatus.confirmed
if booking.status in [BookingStatus.pending, BookingStatus.cancelled]:
booking.status = BookingStatus.confirmed
booking_was_confirmed = True
should_send_email = True
elif booking.status == BookingStatus.confirmed:
# Booking already confirmed, but full payment was just completed
should_send_email = True
# Commit booking status update
# Send booking confirmation email if booking was just confirmed or payment completed
if should_send_email:
try:
from ..utils.mailer import send_email
from ..utils.email_templates import booking_confirmation_email_template
from ..models.system_settings import SystemSettings
from ..models.room import Room
from sqlalchemy.orm import selectinload
import os
from ..config.settings import settings
# Get client URL from settings
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"))
# Get platform currency for email
currency_setting = db.query(SystemSettings).filter(SystemSettings.key == "platform_currency").first()
currency = currency_setting.value if currency_setting and currency_setting.value else "USD"
# Get currency symbol
currency_symbols = {
"USD": "$", "EUR": "", "GBP": "£", "JPY": "¥", "CNY": "¥",
"KRW": "", "SGD": "S$", "THB": "฿", "AUD": "A$", "CAD": "C$",
"VND": "", "INR": "", "CHF": "CHF", "NZD": "NZ$"
}
currency_symbol = currency_symbols.get(currency, currency)
# Load booking with room details for email
booking_with_room = db.query(Booking).options(
selectinload(Booking.room).selectinload(Room.room_type)
).filter(Booking.id == booking_id).first()
room = booking_with_room.room if booking_with_room else None
room_type_name = room.room_type.name if room and room.room_type else "Room"
# Calculate amount paid and remaining due
amount_paid = total_paid
payment_type_str = payment.payment_type.value if payment.payment_type else None
email_html = booking_confirmation_email_template(
booking_number=booking.booking_number,
guest_name=booking.user.full_name if booking.user else "Guest",
room_number=room.room_number if room else "N/A",
room_type=room_type_name,
check_in=booking.check_in_date.strftime("%B %d, %Y") if booking.check_in_date else "N/A",
check_out=booking.check_out_date.strftime("%B %d, %Y") if booking.check_out_date else "N/A",
num_guests=booking.num_guests,
total_price=float(booking.total_price),
requires_deposit=False, # Payment completed, no deposit message needed
deposit_amount=None,
amount_paid=amount_paid,
payment_type=payment_type_str,
client_url=client_url,
currency_symbol=currency_symbol
)
if booking.user:
await send_email(
to=booking.user.email,
subject=f"Booking Confirmed - {booking.booking_number}",
html=email_html
)
logger.info(f"Booking confirmation email sent to {booking.user.email}")
except Exception as email_error:
logger.error(f"Failed to send booking confirmation email: {str(email_error)}")
# Send invoice email if payment is completed and invoice is now paid
from ..utils.mailer import send_email
from ..services.invoice_service import InvoiceService
# Load user for email
from ..models.user import User
user = db.query(User).filter(User.id == booking.user_id).first()
for invoice in invoices:
if invoice.status == InvoiceStatus.paid and invoice.balance_due <= 0:
try:
invoice_dict = InvoiceService.invoice_to_dict(invoice)
invoice_html = _generate_invoice_email_html(invoice_dict, is_proforma=invoice.is_proforma)
invoice_type = "Proforma Invoice" if invoice.is_proforma else "Invoice"
if user:
await send_email(
to=user.email,
subject=f"{invoice_type} {invoice.invoice_number} - Payment Confirmed",
html=invoice_html
)
logger.info(f"{invoice_type} {invoice.invoice_number} sent to {user.email}")
except Exception as email_error:
logger.error(f"Failed to send invoice email: {str(email_error)}")
# Commit booking and invoice status updates
db.commit()
db.refresh(booking)
@@ -335,7 +471,7 @@ class StripeService:
raise ValueError(f"Error confirming payment: {error_msg}")
@staticmethod
def handle_webhook(
async def handle_webhook(
payload: bytes,
signature: str,
db: Session
@@ -375,14 +511,16 @@ class StripeService:
booking_id = metadata.get("booking_id")
if booking_id:
try:
StripeService.confirm_payment(
payment_intent_id=payment_intent_id,
db=db,
booking_id=int(booking_id)
)
except Exception as e:
print(f"Error processing webhook for booking {booking_id}: {str(e)}")
try:
await StripeService.confirm_payment(
payment_intent_id=payment_intent_id,
db=db,
booking_id=int(booking_id)
)
except Exception as e:
import logging
logger = logging.getLogger(__name__)
logger.error(f"Error processing webhook for booking {booking_id}: {str(e)}")
elif event["type"] == "payment_intent.payment_failed":
payment_intent = event["data"]["object"]
@@ -400,6 +538,42 @@ class StripeService:
if payment:
payment.payment_status = PaymentStatus.failed
db.commit()
# Auto-cancel booking when payment fails
booking = db.query(Booking).filter(Booking.id == int(booking_id)).first()
if booking and booking.status != BookingStatus.cancelled:
booking.status = BookingStatus.cancelled
db.commit()
db.refresh(booking)
# Send cancellation email (non-blocking)
try:
if booking.user:
from ..utils.mailer import send_email
from ..utils.email_templates import booking_status_changed_email_template
from ..models.system_settings import SystemSettings
from ..config.settings import settings
import os
# Get client URL from settings
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"))
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}")
return {
"status": "success",