diff --git a/api/main.py b/api/main.py index 05aef6e..b6f9993 100644 --- a/api/main.py +++ b/api/main.py @@ -24,7 +24,7 @@ from circuitforge_core.affiliates import wrap_url as _wrap_affiliate_url from circuitforge_core.api import make_corrections_router as _make_corrections_router from circuitforge_core.api import make_feedback_router as _make_feedback_router from circuitforge_core.config import load_env -from fastapi import Depends, FastAPI, File, HTTPException, Request, Response, UploadFile +from fastapi import Depends, FastAPI, File, HTTPException, Query, Request, Response, UploadFile from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import StreamingResponse from pydantic import BaseModel @@ -34,7 +34,7 @@ from api.ebay_webhook import router as ebay_webhook_router from app.db.models import SavedSearch as SavedSearchModel from app.db.models import ScammerEntry from app.db.store import Store -from app.platforms import SearchFilters +from app.platforms import SUPPORTED_PLATFORMS, SearchFilters from app.platforms.ebay.adapter import EbayAdapter from app.platforms.ebay.auth import EbayTokenManager from app.platforms.ebay.query_builder import expand_queries, parse_groups @@ -674,6 +674,11 @@ def _make_adapter(shared_store: Store, force: str = "auto"): Adapters receive shared_store because they only read/write sellers and market_comps — never listings. Listings are returned and saved by the caller. + + # Platform registry — add new adapters here as platforms are implemented. + # _make_adapter() currently handles eBay only. Phase 2 will add: + # "mercari": MercariAdapter + # "poshmark": PoshmarkAdapter """ client_id, client_secret, env = _ebay_creds() has_creds = bool(client_id and client_secret) @@ -713,8 +718,15 @@ def search( category_id: str = "", # eBay category ID — forwarded to Browse API / scraper _sacat adapter: str = "auto", # "auto" | "api" | "scraper" — override adapter selection refresh: bool = False, # when True, bypass cache read (still writes fresh result) + platform: str = Query("ebay", description="Marketplace platform to search"), session: CloudUser = Depends(get_session), ): + if platform not in SUPPORTED_PLATFORMS: + raise HTTPException( + status_code=422, + detail=f"Platform {platform!r} is not yet supported. Supported: {sorted(SUPPORTED_PLATFORMS)}", + ) + # If the user pasted an eBay listing or checkout URL, extract the item ID # and use it as the search query so the exact item surfaces in results. ebay_item_id = _extract_ebay_item_id(q) @@ -909,8 +921,8 @@ def search( raise HTTPException(status_code=502, detail=f"eBay search failed: {e}") log.info( - "search auth=%s tier=%s adapter=%s pages=%d queries=%d listings=%d q=%r", - _auth_label(session.user_id), session.tier, adapter_used, + "search platform=%s auth=%s tier=%s adapter=%s pages=%d queries=%d listings=%d q=%r", + platform, _auth_label(session.user_id), session.tier, adapter_used, pages, len(ebay_queries), len(listings), q, ) @@ -1073,6 +1085,7 @@ def search_async( category_id: str = "", adapter: str = "auto", refresh: bool = False, # when True, bypass cache read (still writes fresh result) + platform: str = Query("ebay", description="Marketplace platform to search"), session: CloudUser = Depends(get_session), ): """Async variant of GET /api/search. @@ -1088,6 +1101,12 @@ def search_async( "seller": {...}, "market_price": ...} (enrichment updates) None (sentinel — stream finished) """ + if platform not in SUPPORTED_PLATFORMS: + raise HTTPException( + status_code=422, + detail=f"Platform {platform!r} is not yet supported. Supported: {sorted(SUPPORTED_PLATFORMS)}", + ) + # Validate / normalise params — same logic as synchronous endpoint. ebay_item_id = _extract_ebay_item_id(q) if ebay_item_id: @@ -1285,8 +1304,8 @@ def search_async( comps_future.result() log.info( - "async_search auth=%s tier=%s adapter=%s pages=%d listings=%d q=%r", - _auth_label(_user_id), _tier, adapter_used, pages, len(listings), q_norm, + "async_search platform=%s auth=%s tier=%s adapter=%s pages=%d listings=%d q=%r", + platform, _auth_label(_user_id), _tier, adapter_used, pages, len(listings), q_norm, ) shared_store = Store(_shared_db) diff --git a/app/platforms/__init__.py b/app/platforms/__init__.py index 1ed2dd0..fb8dd63 100644 --- a/app/platforms/__init__.py +++ b/app/platforms/__init__.py @@ -7,6 +7,10 @@ from typing import Optional from app.db.models import Listing, Seller +# Single source of truth for platform validation. +# Phase 2 will extend this set as new adapters are implemented. +SUPPORTED_PLATFORMS: frozenset[str] = frozenset({"ebay"}) + @dataclass class SearchFilters: @@ -18,6 +22,8 @@ class SearchFilters: must_include: list[str] = field(default_factory=list) # client-side title filter must_exclude: list[str] = field(default_factory=list) # forwarded to eBay -term AND client-side category_id: Optional[str] = None # eBay category ID (e.g. "27386" = GPUs) + must_include_mode: str = "all" # "all" | "any" | "groups" + adapter: str = "auto" # "auto" | "api" | "scraper" class PlatformAdapter(ABC): diff --git a/web/src/components/SearchProgress.vue b/web/src/components/SearchProgress.vue index 0a6d705..ebfb475 100644 --- a/web/src/components/SearchProgress.vue +++ b/web/src/components/SearchProgress.vue @@ -7,7 +7,7 @@
- Searching eBay for {{ query }}… + Searching {{ platformLabel }} for {{ query }}…
@@ -28,7 +28,19 @@