|
2 | 2 | import json |
3 | 3 | import logging |
4 | 4 | import time |
5 | | -from datetime import datetime, timedelta |
| 5 | +from datetime import date, datetime, timedelta |
6 | 6 | from typing import Any, Dict, List, Optional, Tuple |
7 | 7 |
|
8 | | -from sqlalchemy import asc, desc, func, text |
| 8 | +from sqlalchemy import asc, desc, extract, func, text |
9 | 9 | from sqlalchemy.orm import Session |
10 | 10 |
|
11 | 11 | from src.analytics.models import ( |
@@ -540,7 +540,61 @@ def get_trending_events(self, limit: int = 10, hours: int = 24) -> List[Dict[str |
540 | 540 | _trending_cache = (rows, time.monotonic() + _TRENDING_CACHE_TTL) |
541 | 541 | return rows[:limit] |
542 | 542 |
|
543 | | - def _update_analytics_stats(self, event_id: str, |
| 543 | + def get_scan_heatmap( |
| 544 | + self, |
| 545 | + event_id: str, |
| 546 | + filter_date: Optional[date] = None, |
| 547 | + ) -> Dict[str, Any]: |
| 548 | + """Return hourly scan-density data (24 buckets) for an event. |
| 549 | +
|
| 550 | + Optionally scoped to a single calendar day via *filter_date*. |
| 551 | + Hours with no scans are filled with a count of 0 so the response |
| 552 | + always contains exactly 24 entries. |
| 553 | + """ |
| 554 | + session = None |
| 555 | + try: |
| 556 | + session = get_session() |
| 557 | + |
| 558 | + hour_expr = extract("hour", TicketScan.scan_timestamp) |
| 559 | + query = ( |
| 560 | + session.query( |
| 561 | + hour_expr.label("hour"), |
| 562 | + func.count(TicketScan.id).label("scan_count"), |
| 563 | + ) |
| 564 | + .filter(TicketScan.event_id == event_id) |
| 565 | + ) |
| 566 | + |
| 567 | + if filter_date is not None: |
| 568 | + query = query.filter( |
| 569 | + func.date(TicketScan.scan_timestamp) == filter_date.isoformat() |
| 570 | + ) |
| 571 | + |
| 572 | + rows = query.group_by(hour_expr).all() |
| 573 | + |
| 574 | + hour_counts: Dict[int, int] = { |
| 575 | + int(row.hour): int(row.scan_count) for row in rows |
| 576 | + } |
| 577 | + |
| 578 | + data = [ |
| 579 | + {"hour": h, "scan_count": hour_counts.get(h, 0)} |
| 580 | + for h in range(24) |
| 581 | + ] |
| 582 | + |
| 583 | + peak_hour = max(range(24), key=lambda h: hour_counts.get(h, 0)) |
| 584 | + |
| 585 | + return {"event_id": event_id, "data": data, "peak_hour": peak_hour} |
| 586 | + |
| 587 | + except Exception as e: |
| 588 | + log_error("Failed to get scan heatmap", { |
| 589 | + "event_id": event_id, |
| 590 | + "error": str(e), |
| 591 | + }) |
| 592 | + raise |
| 593 | + finally: |
| 594 | + if session: |
| 595 | + session.close() |
| 596 | + |
| 597 | + def _update_analytics_stats(self, event_id: str, |
544 | 598 | increment_scan: bool = False, is_valid: bool = True, |
545 | 599 | increment_transfer: bool = False, is_successful: bool = True, |
546 | 600 | increment_invalid: bool = False): |
|
0 commit comments