98 lines
3.7 KiB
Python
98 lines
3.7 KiB
Python
import uuid
|
|
from repositories.pocketbase_client import pb_client
|
|
from repositories.repo_config import USE_MOCK_REPOSITORIES
|
|
|
|
COLLECTION = 'products'
|
|
|
|
|
|
class ProductRepository:
|
|
"""Data access for the `products` collection."""
|
|
|
|
def get_latest_for_competitor(self, competitor_id, limit=10):
|
|
"""Returns a competitor's most recently scraped products, newest first."""
|
|
return pb_client.list(
|
|
COLLECTION,
|
|
filter_str=f'competitor_id = "{competitor_id}" && is_active = true',
|
|
sort='-last_seen',
|
|
per_page=limit
|
|
)
|
|
|
|
def get_by_competitor_and_name(self, competitor_id, trip_name):
|
|
"""
|
|
Finds an existing active product for this competitor matching
|
|
trip_name exactly — the dedup key used to decide whether a
|
|
scrape found a genuinely new product or an update to one
|
|
already on record (CLAUDE.md §15/§19 is_new + price diffing).
|
|
Returns None if no match (i.e. this is a new product).
|
|
"""
|
|
if not trip_name:
|
|
return None
|
|
safe_name = trip_name.replace('"', '\\"')
|
|
return pb_client.get_first(
|
|
COLLECTION,
|
|
f'competitor_id = "{competitor_id}" && trip_name = "{safe_name}" && is_active = true'
|
|
)
|
|
|
|
def list_for_tenant(self, tenant_id):
|
|
"""Returns all active products for a tenant — used for embedding/comparable matching."""
|
|
return pb_client.list(COLLECTION, filter_str=f'tenant_id = "{tenant_id}" && is_active = true', per_page=500)
|
|
|
|
def list_all_for_tenant(self, tenant_id):
|
|
"""Returns every product for a tenant regardless of is_active — used for GDPR erasure."""
|
|
return pb_client.list(COLLECTION, filter_str=f'tenant_id = "{tenant_id}"', per_page=500)
|
|
|
|
def create(self, data):
|
|
return pb_client.create(COLLECTION, data)
|
|
|
|
def update(self, product_id, data):
|
|
return pb_client.update(COLLECTION, product_id, data)
|
|
|
|
def delete(self, product_id):
|
|
return pb_client.delete(COLLECTION, product_id)
|
|
|
|
|
|
class MockProductRepository:
|
|
"""In-memory stand-in for ProductRepository."""
|
|
|
|
def __init__(self):
|
|
self._records = {}
|
|
|
|
def get_latest_for_competitor(self, competitor_id, limit=10):
|
|
items = [r for r in self._records.values() if r.get('competitor_id') == competitor_id and r.get('is_active', True)]
|
|
items.sort(key=lambda r: r.get('last_seen') or '', reverse=True)
|
|
return items[:limit]
|
|
|
|
def get_by_competitor_and_name(self, competitor_id, trip_name):
|
|
if not trip_name:
|
|
return None
|
|
return next((
|
|
r for r in self._records.values()
|
|
if r.get('competitor_id') == competitor_id
|
|
and r.get('trip_name') == trip_name
|
|
and r.get('is_active', True)
|
|
), None)
|
|
|
|
def list_for_tenant(self, tenant_id):
|
|
return [r for r in self._records.values() if r.get('tenant_id') == tenant_id and r.get('is_active', True)]
|
|
|
|
def list_all_for_tenant(self, tenant_id):
|
|
return [r for r in self._records.values() if r.get('tenant_id') == tenant_id]
|
|
|
|
def create(self, data):
|
|
record_id = data.get('id') or str(uuid.uuid4())
|
|
record = {'id': record_id, 'is_active': True, **data}
|
|
self._records[record_id] = record
|
|
return record
|
|
|
|
def update(self, product_id, data):
|
|
if product_id not in self._records:
|
|
return None
|
|
self._records[product_id] = {**self._records[product_id], **data}
|
|
return self._records[product_id]
|
|
|
|
def delete(self, product_id):
|
|
return self._records.pop(product_id, None) is not None
|
|
|
|
|
|
product_repository = MockProductRepository() if USE_MOCK_REPOSITORIES else ProductRepository()
|