Formatting
This commit is contained in:
@@ -16,7 +16,12 @@ from .config_loader import load_config
|
||||
from fastapi.responses import HTMLResponse, PlainTextResponse, Response
|
||||
from .models import WixFormSubmission
|
||||
from datetime import datetime, date, timezone
|
||||
from .auth import generate_unique_id, validate_api_key, validate_wix_signature, generate_api_key
|
||||
from .auth import (
|
||||
generate_unique_id,
|
||||
validate_api_key,
|
||||
validate_wix_signature,
|
||||
generate_api_key,
|
||||
)
|
||||
from .rate_limit import (
|
||||
limiter,
|
||||
webhook_limiter,
|
||||
@@ -34,7 +39,12 @@ import os
|
||||
import asyncio
|
||||
import gzip
|
||||
import xml.etree.ElementTree as ET
|
||||
from .alpinebits_server import AlpineBitsClientInfo, AlpineBitsServer, Version, AlpineBitsActionName
|
||||
from .alpinebits_server import (
|
||||
AlpineBitsClientInfo,
|
||||
AlpineBitsServer,
|
||||
Version,
|
||||
AlpineBitsActionName,
|
||||
)
|
||||
import urllib.parse
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
||||
from functools import partial
|
||||
@@ -57,29 +67,31 @@ security_basic = HTTPBasic()
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
# --- Enhanced event dispatcher with hotel-specific routing ---
|
||||
class EventDispatcher:
|
||||
def __init__(self):
|
||||
self.listeners = defaultdict(list)
|
||||
self.hotel_listeners = defaultdict(list) # hotel_code -> list of listeners
|
||||
|
||||
|
||||
def register(self, event_name, func):
|
||||
self.listeners[event_name].append(func)
|
||||
|
||||
|
||||
def register_hotel_listener(self, event_name, hotel_code, func):
|
||||
"""Register a listener for a specific hotel"""
|
||||
self.hotel_listeners[f"{event_name}:{hotel_code}"].append(func)
|
||||
|
||||
|
||||
async def dispatch(self, event_name, *args, **kwargs):
|
||||
for func in self.listeners[event_name]:
|
||||
await func(*args, **kwargs)
|
||||
|
||||
|
||||
async def dispatch_for_hotel(self, event_name, hotel_code, *args, **kwargs):
|
||||
"""Dispatch event only to listeners registered for specific hotel"""
|
||||
key = f"{event_name}:{hotel_code}"
|
||||
for func in self.hotel_listeners[key]:
|
||||
await func(*args, **kwargs)
|
||||
|
||||
|
||||
event_dispatcher = EventDispatcher()
|
||||
|
||||
# Load config at startup
|
||||
@@ -92,30 +104,41 @@ async def push_listener(customer: DBCustomer, reservation: DBReservation, hotel)
|
||||
"""
|
||||
push_endpoint = hotel.get("push_endpoint")
|
||||
if not push_endpoint:
|
||||
_LOGGER.warning(f"No push endpoint configured for hotel {hotel.get('hotel_id')}")
|
||||
_LOGGER.warning(
|
||||
f"No push endpoint configured for hotel {hotel.get('hotel_id')}"
|
||||
)
|
||||
return
|
||||
|
||||
server: AlpineBitsServer = app.state.alpine_bits_server
|
||||
hotel_id = hotel['hotel_id']
|
||||
hotel_id = hotel["hotel_id"]
|
||||
reservation_hotel_id = reservation.hotel_code
|
||||
|
||||
|
||||
# Double-check hotel matching (should be guaranteed by dispatcher)
|
||||
if hotel_id != reservation_hotel_id:
|
||||
_LOGGER.warning(f"Hotel ID mismatch: listener for {hotel_id}, reservation for {reservation_hotel_id}")
|
||||
_LOGGER.warning(
|
||||
f"Hotel ID mismatch: listener for {hotel_id}, reservation for {reservation_hotel_id}"
|
||||
)
|
||||
return
|
||||
|
||||
_LOGGER.info(f"Processing push notification for hotel {hotel_id}, reservation {reservation.unique_id}")
|
||||
|
||||
_LOGGER.info(
|
||||
f"Processing push notification for hotel {hotel_id}, reservation {reservation.unique_id}"
|
||||
)
|
||||
|
||||
# Prepare payload for push notification
|
||||
|
||||
|
||||
request = await server.handle_request(request_action_name=AlpineBitsActionName.OTA_HOTEL_RES_NOTIF_GUEST_REQUESTS.request_name, request_xml=(reservation, customer), client_info=None, version=Version.V2024_10)
|
||||
request = await server.handle_request(
|
||||
request_action_name=AlpineBitsActionName.OTA_HOTEL_RES_NOTIF_GUEST_REQUESTS.request_name,
|
||||
request_xml=(reservation, customer),
|
||||
client_info=None,
|
||||
version=Version.V2024_10,
|
||||
)
|
||||
|
||||
if request.status_code != 200:
|
||||
_LOGGER.error(f"Failed to generate push request for hotel {hotel_id}, reservation {reservation.unique_id}: {request.xml_content}")
|
||||
_LOGGER.error(
|
||||
f"Failed to generate push request for hotel {hotel_id}, reservation {reservation.unique_id}: {request.xml_content}"
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
# save push request to file
|
||||
|
||||
logs_dir = "logs/push_requests"
|
||||
@@ -126,28 +149,37 @@ async def push_listener(customer: DBCustomer, reservation: DBReservation, hotel)
|
||||
f"Created directory owner: uid:{stat_info.st_uid}, gid:{stat_info.st_gid}"
|
||||
)
|
||||
_LOGGER.info(f"Directory mode: {oct(stat_info.st_mode)[-3:]}")
|
||||
log_filename = (
|
||||
f"{logs_dir}/alpinebits_push_{hotel_id}_{reservation.unique_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xml"
|
||||
)
|
||||
|
||||
log_filename = f"{logs_dir}/alpinebits_push_{hotel_id}_{reservation.unique_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xml"
|
||||
|
||||
with open(log_filename, "w", encoding="utf-8") as f:
|
||||
f.write(request.xml_content)
|
||||
return
|
||||
|
||||
headers = {"Authorization": f"Bearer {push_endpoint.get('token','')}"} if push_endpoint.get('token') else {}
|
||||
headers = (
|
||||
{"Authorization": f"Bearer {push_endpoint.get('token', '')}"}
|
||||
if push_endpoint.get("token")
|
||||
else {}
|
||||
)
|
||||
""
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.post(push_endpoint["url"], json=payload, headers=headers, timeout=10)
|
||||
_LOGGER.info(f"Push event fired to {push_endpoint['url']} for hotel {hotel['hotel_id']}, status: {resp.status_code}")
|
||||
|
||||
resp = await client.post(
|
||||
push_endpoint["url"], json=payload, headers=headers, timeout=10
|
||||
)
|
||||
_LOGGER.info(
|
||||
f"Push event fired to {push_endpoint['url']} for hotel {hotel['hotel_id']}, status: {resp.status_code}"
|
||||
)
|
||||
|
||||
if resp.status_code not in [200, 201, 202]:
|
||||
_LOGGER.warning(f"Push endpoint returned non-success status {resp.status_code}: {resp.text}")
|
||||
|
||||
_LOGGER.warning(
|
||||
f"Push endpoint returned non-success status {resp.status_code}: {resp.text}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
_LOGGER.error(f"Push event failed for hotel {hotel['hotel_id']}: {e}")
|
||||
# Optionally implement retry logic here@asynccontextmanager
|
||||
|
||||
|
||||
async def lifespan(app: FastAPI):
|
||||
# Setup DB
|
||||
|
||||
@@ -167,20 +199,19 @@ async def lifespan(app: FastAPI):
|
||||
app.state.alpine_bits_server = AlpineBitsServer(config)
|
||||
app.state.event_dispatcher = event_dispatcher
|
||||
|
||||
|
||||
# Register push listeners for hotels with push_endpoint
|
||||
for hotel in config.get("alpine_bits_auth", []):
|
||||
push_endpoint = hotel.get("push_endpoint")
|
||||
hotel_id = hotel.get("hotel_id")
|
||||
|
||||
|
||||
if push_endpoint and hotel_id:
|
||||
# Register hotel-specific listener
|
||||
event_dispatcher.register_hotel_listener(
|
||||
"form_processed",
|
||||
hotel_id,
|
||||
partial(push_listener, hotel=hotel)
|
||||
"form_processed", hotel_id, partial(push_listener, hotel=hotel)
|
||||
)
|
||||
_LOGGER.info(
|
||||
f"Registered push listener for hotel {hotel_id} with endpoint {push_endpoint.get('url')}"
|
||||
)
|
||||
_LOGGER.info(f"Registered push listener for hotel {hotel_id} with endpoint {push_endpoint.get('url')}")
|
||||
elif push_endpoint and not hotel_id:
|
||||
_LOGGER.warning(f"Hotel has push_endpoint but no hotel_id: {hotel}")
|
||||
elif hotel_id and not push_endpoint:
|
||||
@@ -351,7 +382,7 @@ async def process_wix_form_submission(request: Request, data: Dict[str, Any], db
|
||||
name_prefix = data.get("field:anrede")
|
||||
email_newsletter_string = data.get("field:form_field_5a7b", "")
|
||||
yes_values = {"Selezionato", "Angekreuzt", "Checked"}
|
||||
email_newsletter = (email_newsletter_string in yes_values)
|
||||
email_newsletter = email_newsletter_string in yes_values
|
||||
address_line = None
|
||||
city_name = None
|
||||
postal_code = None
|
||||
@@ -397,15 +428,13 @@ async def process_wix_form_submission(request: Request, data: Dict[str, Any], db
|
||||
]
|
||||
|
||||
# get submissionId and ensure max length 35. Generate one if not present
|
||||
|
||||
|
||||
unique_id = data.get("submissionId", generate_unique_id())
|
||||
|
||||
if len(unique_id) > 32:
|
||||
# strip to first 35 chars
|
||||
unique_id = unique_id[:32]
|
||||
|
||||
|
||||
|
||||
# use database session
|
||||
|
||||
# Save all relevant data to DB (including new fields)
|
||||
@@ -429,23 +458,22 @@ async def process_wix_form_submission(request: Request, data: Dict[str, Any], db
|
||||
)
|
||||
db.add(db_customer)
|
||||
await db.flush() # This assigns db_customer.id without committing
|
||||
#await db.refresh(db_customer)
|
||||
|
||||
# await db.refresh(db_customer)
|
||||
|
||||
# Determine hotel_code and hotel_name
|
||||
# Priority: 1) Form field, 2) Configuration default, 3) Hardcoded fallback
|
||||
hotel_code = (
|
||||
data.get("field:hotelid") or
|
||||
data.get("hotelid") or
|
||||
request.app.state.config.get("default_hotel_code") or
|
||||
"123" # fallback
|
||||
data.get("field:hotelid")
|
||||
or data.get("hotelid")
|
||||
or request.app.state.config.get("default_hotel_code")
|
||||
or "123" # fallback
|
||||
)
|
||||
|
||||
|
||||
hotel_name = (
|
||||
data.get("field:hotelname") or
|
||||
data.get("hotelname") or
|
||||
request.app.state.config.get("default_hotel_name") or
|
||||
"Frangart Inn" # fallback
|
||||
data.get("field:hotelname")
|
||||
or data.get("hotelname")
|
||||
or request.app.state.config.get("default_hotel_name")
|
||||
or "Frangart Inn" # fallback
|
||||
)
|
||||
|
||||
db_reservation = DBReservation(
|
||||
@@ -472,22 +500,24 @@ async def process_wix_form_submission(request: Request, data: Dict[str, Any], db
|
||||
db.add(db_reservation)
|
||||
await db.commit()
|
||||
await db.refresh(db_reservation)
|
||||
|
||||
|
||||
|
||||
async def push_event():
|
||||
# Fire event for listeners (push, etc.) - hotel-specific dispatch
|
||||
dispatcher = getattr(request.app.state, "event_dispatcher", None)
|
||||
if dispatcher:
|
||||
# Get hotel_code from reservation to target the right listeners
|
||||
hotel_code = getattr(db_reservation, 'hotel_code', None)
|
||||
hotel_code = getattr(db_reservation, "hotel_code", None)
|
||||
if hotel_code and hotel_code.strip():
|
||||
await dispatcher.dispatch_for_hotel("form_processed", hotel_code, db_customer, db_reservation)
|
||||
await dispatcher.dispatch_for_hotel(
|
||||
"form_processed", hotel_code, db_customer, db_reservation
|
||||
)
|
||||
_LOGGER.info(f"Dispatched form_processed event for hotel {hotel_code}")
|
||||
else:
|
||||
_LOGGER.warning("No hotel_code in reservation, skipping push notifications")
|
||||
_LOGGER.warning(
|
||||
"No hotel_code in reservation, skipping push notifications"
|
||||
)
|
||||
|
||||
asyncio.create_task(push_event())
|
||||
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
@@ -517,9 +547,7 @@ async def handle_wix_form(
|
||||
|
||||
traceback_str = traceback.format_exc()
|
||||
_LOGGER.error(f"Stack trace for handle_wix_form: {traceback_str}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Error processing Wix form data"
|
||||
)
|
||||
raise HTTPException(status_code=500, detail=f"Error processing Wix form data")
|
||||
|
||||
|
||||
@api_router.post("/webhook/wix-form/test")
|
||||
@@ -535,9 +563,7 @@ async def handle_wix_form_test(
|
||||
return await process_wix_form_submission(request, data, db_session)
|
||||
except Exception as e:
|
||||
_LOGGER.error(f"Error in handle_wix_form_test: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Error processing test data"
|
||||
)
|
||||
raise HTTPException(status_code=500, detail=f"Error processing test data")
|
||||
|
||||
|
||||
@api_router.post("/admin/generate-api-key")
|
||||
@@ -773,7 +799,9 @@ async def alpinebits_server_handshake(
|
||||
|
||||
username, password = credentials_tupel
|
||||
|
||||
client_info = AlpineBitsClientInfo(username=username, password=password, client_id=client_id)
|
||||
client_info = AlpineBitsClientInfo(
|
||||
username=username, password=password, client_id=client_id
|
||||
)
|
||||
|
||||
# Create successful handshake response
|
||||
response = await server.handle_request(
|
||||
|
||||
Reference in New Issue
Block a user