Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions raley_assistant/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,25 @@ def get_cart(client: CurlClient) -> dict:
# ============================================================================


def get_store_id(client: CurlClient) -> str:
"""Get the current store/fulfillment location ID from the session.

Returns store identifier string, or empty string if unavailable.
"""
status, data = client.get(f"{BASE_URL}/api/auth/session")
if status != 200 or not isinstance(data, dict):
return ""
# The session API nests store info in various places depending on fulfillment type
user = data.get("user", {})
store = user.get("store", {})
store_id = store.get("id", "") or store.get("key", "")
if not store_id:
# Fallback: check fulfillment location
fulfillment = user.get("fulfillment", {})
store_id = fulfillment.get("storeId", "") or fulfillment.get("locationId", "")
return str(store_id) if store_id else ""


def check_session(client: CurlClient) -> dict | None:
"""Check if session is valid. Returns user data or None."""
status, data = client.get(f"{BASE_URL}/api/auth/session")
Expand Down
63 changes: 58 additions & 5 deletions raley_assistant/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
unit_oz REAL,
price_per_oz REAL,
last_seen TEXT NOT NULL,
first_seen TEXT NOT NULL
first_seen TEXT NOT NULL,
store_id TEXT DEFAULT ''
);

CREATE TABLE IF NOT EXISTS price_history (
Expand Down Expand Up @@ -100,6 +101,15 @@ class ProductHistory:
last_seen: str


def _migrate(conn: sqlite3.Connection) -> None:
"""Apply incremental schema migrations for existing databases."""
# Add store_id to products table if it doesn't exist (migration from pre-0.4)
cols = {row[1] for row in conn.execute("PRAGMA table_info(products)").fetchall()}
if "store_id" not in cols:
conn.execute("ALTER TABLE products ADD COLUMN store_id TEXT DEFAULT ''")
conn.commit()


def get_connection() -> sqlite3.Connection:
"""Get database connection, creating schema if needed."""
DB_DIR.mkdir(parents=True, exist_ok=True)
Expand All @@ -108,10 +118,11 @@ def get_connection() -> sqlite3.Connection:
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA foreign_keys=ON")
conn.executescript(SCHEMA)
_migrate(conn)
return conn


def sync_products_from_search(conn: sqlite3.Connection, products: list) -> int:
def sync_products_from_search(conn: sqlite3.Connection, products: list, store_id: str = "") -> int:
"""Sync Product objects from a search into the local DB.

Upserts product records and appends price history observations.
Expand All @@ -127,8 +138,8 @@ def sync_products_from_search(conn: sqlite3.Connection, products: list) -> int:
conn.execute(
"""
INSERT INTO products (sku, name, brand, price_cents, sale_price_cents,
size, unit_oz, price_per_oz, last_seen, first_seen)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
size, unit_oz, price_per_oz, last_seen, first_seen, store_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(sku) DO UPDATE SET
name = excluded.name,
brand = excluded.brand,
Expand All @@ -137,7 +148,8 @@ def sync_products_from_search(conn: sqlite3.Connection, products: list) -> int:
size = excluded.size,
unit_oz = excluded.unit_oz,
price_per_oz = excluded.price_per_oz,
last_seen = excluded.last_seen
last_seen = excluded.last_seen,
store_id = CASE WHEN excluded.store_id != '' THEN excluded.store_id ELSE products.store_id END
""",
(
p.sku,
Expand All @@ -150,6 +162,7 @@ def sync_products_from_search(conn: sqlite3.Connection, products: list) -> int:
p.price_per_oz,
now,
now,
store_id,
),
)

Expand Down Expand Up @@ -385,6 +398,46 @@ def is_good_deal(
return False, f"Near average price (${avg/100:.2f})."


def check_store_mismatch(conn: sqlite3.Connection, current_store_id: str) -> dict | None:
"""Check if cached products are from a different store.

Returns a warning dict if most cached products have a different store_id,
or None if no mismatch detected.
"""
if not current_store_id:
return None

row = conn.execute(
"""
SELECT store_id, COUNT(*) as cnt
FROM products
WHERE store_id != '' AND store_id != ?
GROUP BY store_id
ORDER BY cnt DESC
LIMIT 1
""",
(current_store_id,),
).fetchone()

if not row:
return None

total = conn.execute("SELECT COUNT(*) as c FROM products WHERE store_id != ''").fetchone()["c"]
mismatched = row["cnt"]

if mismatched > 0 and total > 0 and mismatched / total > 0.5:
return {
"warning": "store_changed",
"cached_store": row["store_id"],
"current_store": current_store_id,
"stale_products": mismatched,
"message": f"Cache has {mismatched} products from a different store. "
f"Run price clear=true to refresh.",
}

return None


def search_products_local(
conn: sqlite3.Connection, query: str, limit: int = 10
) -> list[dict]:
Expand Down
Loading