diff --git a/backend/app/scheduler.py b/backend/app/scheduler.py index c6d7783..7c66c4b 100644 --- a/backend/app/scheduler.py +++ b/backend/app/scheduler.py @@ -1,11 +1,12 @@ -"""Background scheduler for domain checks, TLD price scraping, and notifications.""" +"""Background scheduler for domain checks, TLD price scraping, auctions, and notifications.""" import asyncio import logging -from datetime import datetime +from datetime import datetime, timedelta +from typing import TYPE_CHECKING from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.triggers.cron import CronTrigger -from sqlalchemy import select +from sqlalchemy import select, and_ from app.config import get_settings from app.database import AsyncSessionLocal @@ -16,6 +17,10 @@ from app.services.domain_checker import domain_checker from app.services.email_service import email_service from app.services.price_tracker import price_tracker +if TYPE_CHECKING: + from app.models.sniper_alert import SniperAlert + from app.models.auction import DomainAuction + logger = logging.getLogger(__name__) settings = get_settings() @@ -315,7 +320,164 @@ async def scrape_auctions(): if result.get('errors'): logger.warning(f"Scrape errors: {result['errors']}") + + # Match new auctions against Sniper Alerts + if result['total_new'] > 0: + await match_sniper_alerts() except Exception as e: logger.exception(f"Auction scrape failed: {e}") + +async def match_sniper_alerts(): + """Match active sniper alerts against current auctions and notify users.""" + from app.models.sniper_alert import SniperAlert, SniperAlertMatch + from app.models.auction import DomainAuction + + logger.info("Matching sniper alerts against new auctions...") + + try: + async with AsyncSessionLocal() as db: + # Get all active sniper alerts + alerts_result = await db.execute( + select(SniperAlert).where(SniperAlert.is_active == True) + ) + alerts = alerts_result.scalars().all() + + if not alerts: + logger.info("No active sniper alerts to match") + return + + # Get recent auctions (added in last 2 hours) + cutoff = datetime.utcnow() - timedelta(hours=2) + auctions_result = await db.execute( + select(DomainAuction).where( + and_( + DomainAuction.is_active == True, + DomainAuction.scraped_at >= cutoff, + ) + ) + ) + auctions = auctions_result.scalars().all() + + if not auctions: + logger.info("No recent auctions to match against") + return + + matches_created = 0 + notifications_sent = 0 + + for alert in alerts: + matching_auctions = [] + + for auction in auctions: + if _auction_matches_alert(auction, alert): + matching_auctions.append(auction) + + if matching_auctions: + for auction in matching_auctions: + # Check if this match already exists + existing = await db.execute( + select(SniperAlertMatch).where( + and_( + SniperAlertMatch.alert_id == alert.id, + SniperAlertMatch.domain == auction.domain, + ) + ) + ) + if existing.scalar_one_or_none(): + continue + + # Create new match + match = SniperAlertMatch( + alert_id=alert.id, + domain=auction.domain, + platform=auction.platform, + current_bid=auction.current_bid, + end_time=auction.end_time, + auction_url=auction.auction_url, + matched_at=datetime.utcnow(), + ) + db.add(match) + matches_created += 1 + + # Update alert last_triggered + alert.last_triggered = datetime.utcnow() + + # Send notification if enabled + if alert.notify_email: + try: + user_result = await db.execute( + select(User).where(User.id == alert.user_id) + ) + user = user_result.scalar_one_or_none() + + if user and email_service.is_enabled: + # Send email with matching domains + domains_list = ", ".join([a.domain for a in matching_auctions[:5]]) + await email_service.send_email( + to_email=user.email, + subject=f"🎯 Sniper Alert: {len(matching_auctions)} matching domains found!", + html_content=f""" +
We found {len(matching_auctions)} domains matching your criteria:
+