Backend:
- Migrations 013-015: eBay user tokens, monitor settings on saved_searches
(monitor_enabled, poll_interval_min, min_trust_score, last_checked_at),
watch_alerts table with UNIQUE dedup on (saved_search_id, platform_listing_id),
active_monitors registry for cross-user polling
- WatchAlert model + store methods: upsert_alert, list_alerts, dismiss_alert,
count_undismissed_alerts, dismiss_all_alerts, list_active_monitors
- monitor.py: run_monitor_search() using TrustScorer.score_batch(); should_alert()
with BIN/auction/partial-score logic (auction window = 24h, partial +10 buffer)
- PATCH /api/saved-searches/{id}/monitor, GET /api/alerts, POST /api/alerts/*/dismiss
- Background polling loop at startup (asyncio.to_thread every 60s check cycle)
- ebay/adapter.py: enrich_seller_trading_api() via Trading API GetUser (OAuth token)
- nginx: raise proxy_read_timeout to 120s for slow eBay search responses
Frontend:
- AlertBell component: bell button + unread badge + panel with dismiss/clear-all;
polls /api/alerts every 2 minutes; aria-live announcement on count change
- alerts.ts Pinia store: fetchAlerts, dismiss, dismissAll
- SavedSearchesView: monitor toggle + poll interval + min trust score controls
- SettingsView: eBay OAuth connect/disconnect section
- AppNav: AlertBell wired for logged-in and local-tier users
Tests: 24 monitor tests (should_alert branches, store alert CRUD, run_monitor_search
with mocked adapter); fix browser_pool test assertions for new wait_for_* params.
125 lines
3.6 KiB
Python
125 lines
3.6 KiB
Python
"""Dataclasses for all Snipe domain objects."""
|
||
from __future__ import annotations
|
||
|
||
from dataclasses import dataclass, field
|
||
from typing import Optional
|
||
|
||
|
||
@dataclass
|
||
class Seller:
|
||
platform: str
|
||
platform_seller_id: str
|
||
username: str
|
||
account_age_days: Optional[int] # None = not yet fetched (scraper tier)
|
||
feedback_count: int
|
||
feedback_ratio: float # 0.0–1.0
|
||
category_history_json: str # JSON blob of past category sales
|
||
id: Optional[int] = None
|
||
fetched_at: Optional[str] = None
|
||
|
||
|
||
@dataclass
|
||
class Listing:
|
||
platform: str
|
||
platform_listing_id: str
|
||
title: str
|
||
price: float
|
||
currency: str
|
||
condition: str
|
||
seller_platform_id: str
|
||
url: str
|
||
photo_urls: list[str] = field(default_factory=list)
|
||
listing_age_days: int = 0
|
||
buying_format: str = "fixed_price" # "fixed_price", "auction", "best_offer"
|
||
ends_at: Optional[str] = None # ISO8601 auction end time; None for fixed-price
|
||
id: Optional[int] = None
|
||
fetched_at: Optional[str] = None
|
||
trust_score_id: Optional[int] = None
|
||
category_name: Optional[str] = None # leaf category from eBay API (e.g. "Graphics/Video Cards")
|
||
# Staging DB fields — populated from DB after upsert
|
||
first_seen_at: Optional[str] = None
|
||
last_seen_at: Optional[str] = None
|
||
times_seen: int = 1
|
||
price_at_first_seen: Optional[float] = None
|
||
|
||
|
||
@dataclass
|
||
class TrustScore:
|
||
listing_id: int
|
||
composite_score: int # 0–100
|
||
account_age_score: int # 0–20
|
||
feedback_count_score: int # 0–20
|
||
feedback_ratio_score: int # 0–20
|
||
price_vs_market_score: int # 0–20
|
||
category_history_score: int # 0–20
|
||
photo_hash_duplicate: bool = False
|
||
photo_analysis_json: Optional[str] = None
|
||
red_flags_json: str = "[]"
|
||
score_is_partial: bool = False
|
||
id: Optional[int] = None
|
||
scored_at: Optional[str] = None
|
||
|
||
|
||
@dataclass
|
||
class MarketComp:
|
||
platform: str
|
||
query_hash: str
|
||
median_price: float
|
||
sample_count: int
|
||
expires_at: str # ISO8601 — checked against current time
|
||
id: Optional[int] = None
|
||
fetched_at: Optional[str] = None
|
||
|
||
|
||
@dataclass
|
||
class SavedSearch:
|
||
"""Schema scaffolded in v0.1; background monitoring wired in v0.2."""
|
||
name: str
|
||
query: str
|
||
platform: str
|
||
filters_json: str = "{}"
|
||
id: Optional[int] = None
|
||
created_at: Optional[str] = None
|
||
last_run_at: Optional[str] = None
|
||
# Monitor settings (migration 014)
|
||
monitor_enabled: bool = False
|
||
poll_interval_min: int = 60
|
||
min_trust_score: int = 60
|
||
last_checked_at: Optional[str] = None
|
||
|
||
|
||
@dataclass
|
||
class WatchAlert:
|
||
"""A new listing surfaced by the background monitor for a saved search."""
|
||
saved_search_id: int
|
||
platform_listing_id: str
|
||
title: str
|
||
price: float
|
||
trust_score: int
|
||
currency: str = "USD"
|
||
url: Optional[str] = None
|
||
id: Optional[int] = None
|
||
first_alerted_at: Optional[str] = None
|
||
dismissed_at: Optional[str] = None
|
||
|
||
|
||
@dataclass
|
||
class ScammerEntry:
|
||
"""A seller manually or community-flagged as a known scammer."""
|
||
platform: str
|
||
platform_seller_id: str
|
||
username: str
|
||
reason: Optional[str] = None
|
||
source: str = "manual" # "manual" | "csv_import" | "community"
|
||
id: Optional[int] = None
|
||
created_at: Optional[str] = None
|
||
|
||
|
||
@dataclass
|
||
class PhotoHash:
|
||
"""Perceptual hash store for cross-search dedup (v0.2+). Schema scaffolded in v0.1."""
|
||
listing_id: int
|
||
photo_url: str
|
||
phash: str # hex string from imagehash
|
||
id: Optional[int] = None
|
||
first_seen_at: Optional[str] = None
|