From a8c441ea6f41a3d64bb30f69e14b333dfd8ecb1e Mon Sep 17 00:00:00 2001 From: Jonas Linter <{email_address}> Date: Wed, 15 Oct 2025 09:09:07 +0200 Subject: [PATCH] Stats collector for email monitoring --- src/alpine_bits_python/api.py | 12 ++- src/alpine_bits_python/email_monitoring.py | 110 ++++++++++++++++++++- 2 files changed, 120 insertions(+), 2 deletions(-) diff --git a/src/alpine_bits_python/api.py b/src/alpine_bits_python/api.py index 03e6b1b..1679d94 100644 --- a/src/alpine_bits_python/api.py +++ b/src/alpine_bits_python/api.py @@ -32,6 +32,7 @@ from .customer_service import CustomerService from .db import Base, get_database_url from .db import Customer as DBCustomer from .db import Reservation as DBReservation +from .email_monitoring import ReservationStatsCollector from .email_service import create_email_service from .logging_config import get_logger, setup_logging from .rate_limit import ( @@ -245,8 +246,17 @@ async def lifespan(app: FastAPI): else: _LOGGER.info("All existing customers already have hashed data") - # Start daily report scheduler if enabled + # Initialize and hook up stats collector for daily reports if report_scheduler: + stats_collector = ReservationStatsCollector( + async_sessionmaker=AsyncSessionLocal, + config=config, + ) + # Hook up the stats collector to the report scheduler + report_scheduler.set_stats_collector(stats_collector.collect_stats) + _LOGGER.info("Stats collector initialized and hooked up to report scheduler") + + # Start daily report scheduler report_scheduler.start() _LOGGER.info("Daily report scheduler started") diff --git a/src/alpine_bits_python/email_monitoring.py b/src/alpine_bits_python/email_monitoring.py index ea8349a..8777748 100644 --- a/src/alpine_bits_python/email_monitoring.py +++ b/src/alpine_bits_python/email_monitoring.py @@ -7,10 +7,14 @@ email alerts based on configurable thresholds and time windows. import asyncio import logging import threading -from collections import deque +from collections import defaultdict, deque from datetime import datetime, timedelta from typing import Any +from sqlalchemy import func, select +from sqlalchemy.ext.asyncio import async_sessionmaker + +from .db import Reservation from .email_service import EmailService from .logging_config import get_logger @@ -449,3 +453,107 @@ class DailyReportScheduler: """ self._stats_collector = collector + + +class ReservationStatsCollector: + """Collects reservation statistics per hotel for daily reports. + + This collector queries the database for reservations created since the last + report and aggregates them by hotel. It includes hotel_code and hotel_name + from the configuration. + """ + + def __init__( + self, + async_sessionmaker: async_sessionmaker, + config: dict[str, Any], + ): + """Initialize the stats collector. + + Args: + async_sessionmaker: SQLAlchemy async session maker + config: Application configuration containing hotel information + + """ + self.async_sessionmaker = async_sessionmaker + self.config = config + self._last_report_time = datetime.now() + + # Build hotel mapping from config + self._hotel_map = {} + for hotel in config.get("alpine_bits_auth", []): + hotel_id = hotel.get("hotel_id") + hotel_name = hotel.get("hotel_name") + if hotel_id: + self._hotel_map[hotel_id] = hotel_name or "Unknown Hotel" + + _LOGGER.info( + "ReservationStatsCollector initialized with %d hotels", + len(self._hotel_map), + ) + + async def collect_stats(self) -> dict[str, Any]: + """Collect reservation statistics for the reporting period. + + Returns: + Dictionary with statistics including reservations per hotel + + """ + now = datetime.now() + period_start = self._last_report_time + period_end = now + + _LOGGER.info( + "Collecting reservation stats from %s to %s", + period_start.strftime("%Y-%m-%d %H:%M:%S"), + period_end.strftime("%Y-%m-%d %H:%M:%S"), + ) + + async with self.async_sessionmaker() as session: + # Query reservations created in the reporting period + result = await session.execute( + select(Reservation.hotel_code, func.count(Reservation.id)) + .where(Reservation.created_at >= period_start) + .where(Reservation.created_at < period_end) + .group_by(Reservation.hotel_code) + ) + + hotel_counts = dict(result.all()) + + # Build stats with hotel names from config + hotels_stats = [] + total_reservations = 0 + + for hotel_code, count in hotel_counts.items(): + hotel_name = self._hotel_map.get(hotel_code, "Unknown Hotel") + hotels_stats.append( + { + "hotel_code": hotel_code, + "hotel_name": hotel_name, + "reservations": count, + } + ) + total_reservations += count + + # Sort by reservation count descending + hotels_stats.sort(key=lambda x: x["reservations"], reverse=True) + + # Update last report time + self._last_report_time = now + + stats = { + "reporting_period": { + "start": period_start.strftime("%Y-%m-%d %H:%M:%S"), + "end": period_end.strftime("%Y-%m-%d %H:%M:%S"), + }, + "total_reservations": total_reservations, + "hotels": hotels_stats, + } + + _LOGGER.info( + "Collected stats: %d total reservations across %d hotels", + total_reservations, + len(hotels_stats), + ) + + return stats