snipe/app/tiers.py
pyr0ball e93e3de207 feat: scammer blocklist, search/listing UI overhaul, tier refactor
**Scammer blocklist**
- migration 006: scammer_blocklist table (platform + seller_id unique key,
  source: manual|csv_import|community)
- ScammerEntry dataclass + Store.add/remove/list_blocklist methods
- blocklist.ts Pinia store — CRUD, export CSV, import CSV with validation
- BlocklistView.vue — list with search, export/import, bulk-remove; sellers
  show on ListingCard with force-score-0 badge
- API: GET/POST/DELETE /api/blocklist + CSV export/import endpoints
- Router: /blocklist route added; AppNav link

**Migration renumber**
- 002_background_tasks.sql → 007_background_tasks.sql (correct sequence
  after blocklist; idempotent CREATE IF NOT EXISTS safe for existing DBs)

**Search + listing UI overhaul**
- SearchView.vue: keyword expansion preview, filter chips for condition/
  format/price, saved-search quick-run button, paginated results
- ListingCard.vue: trust tier badge, scammer flag overlay, photo count
  chip, quick-block button, save-to-search action
- savedSearches store: optimistic update on run, last-run timestamp

**Tier refactor**
- tiers.py: full rewrite with docstring ladder, BYOK LOCAL_VISION_UNLOCKABLE
  flag, intentionally-free list with rationale (scammer_db, saved_searches,
  market_comps free to maximise adoption)

**Trust aggregator + scraper**
- aggregator.py: blocklist check short-circuits scoring to 0/BAD_ACTOR
- scraper.py: listing format detection, photo count, improved title parsing

**Theme**
- theme.css: trust tier color tokens, badge variants, blocklist badge
2026-04-03 19:08:54 -07:00

72 lines
2.8 KiB
Python

"""Snipe feature gates. Delegates to circuitforge_core.tiers.
Tier ladder: free < paid < premium
Ultra is not used in Snipe — auto-bidding is the highest-impact feature and is Premium.
BYOK unlock analog: LOCAL_VISION_UNLOCKABLE — photo_analysis and serial_number_check
unlock when the user has a local vision model (moondream2 (MD2) or equivalent).
Intentionally ungated (free for all):
- metadata_trust_scoring — core value prop; wide adoption preferred
- hash_dedup — infrastructure, not a differentiator
- market_comps — useful enough to drive signups; not scarce
- scammer_db — community data is more valuable with wider reach
- saved_searches — retention feature; friction cost outweighs gate value
"""
from __future__ import annotations
from circuitforge_core.tiers import can_use as _core_can_use, TIERS # noqa: F401
# Feature key → minimum tier required.
FEATURES: dict[str, str] = {
# Paid tier
"photo_analysis": "paid",
"serial_number_check": "paid",
"ai_image_detection": "paid",
"reverse_image_search": "paid",
"ebay_oauth": "paid", # full trust scores via eBay Trading API
"background_monitoring": "paid", # limited at Paid; see LIMITS below
# Premium tier
"auto_bidding": "premium",
}
# Per-feature usage limits by tier. None = unlimited.
# Call get_limit(feature, tier) at enforcement points (e.g. before creating a new monitor).
LIMITS: dict[tuple[str, str], int | None] = {
("background_monitoring", "paid"): 5,
("background_monitoring", "premium"): 25,
}
# Unlock photo_analysis and serial_number_check when user has a local vision model.
# Same policy as Peregrine's BYOK_UNLOCKABLE: user is providing the compute.
LOCAL_VISION_UNLOCKABLE: frozenset[str] = frozenset({
"photo_analysis",
"serial_number_check",
})
def can_use(
feature: str,
tier: str = "free",
has_byok: bool = False,
has_local_vision: bool = False,
) -> bool:
if has_local_vision and feature in LOCAL_VISION_UNLOCKABLE:
return True
return _core_can_use(feature, tier, has_byok=has_byok, _features=FEATURES)
def get_limit(feature: str, tier: str) -> int | None:
"""Return the usage limit for a feature at the given tier.
Returns None if the feature is unlimited at this tier.
Returns None if the feature has no entry in LIMITS (treat as unlimited).
Call can_use() first — get_limit() does not check tier eligibility.
Example:
if can_use("background_monitoring", tier):
limit = get_limit("background_monitoring", tier)
if limit is not None and current_count >= limit:
raise LimitExceeded(f"Paid tier allows {limit} active monitors. Upgrade to Premium for unlimited.")
"""
return LIMITS.get((feature, tier))