updates
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user