- Cleaned 137+ fake engine-displacement models from supplier imports (v3/v4 scripts: Chevrolet, Ford, Chrysler, Dodge, Jeep, Nissan, etc.) - Removed 1,251+ corrupted models (INT. prefixes, year-suffix, torque specs, empty names, trailing-year variants) - Migrated supplier tables to master DB (supplier_catalog, supplier_catalog_compat, supplier_catalog_interchange) - Fixed _get_mye_ids_with_parts() to query supplier_catalog_compat from master DB so supplier-only vehicles appear for all tenants - Added fuzzy model matcher with parenthesis stripping, noise suffix removal, compact matching, prefix/substring fallback, model aliases, and ±3 year proximity - Matched compat rows: KEEP GREEN +14,152, KNADIAN +3,021, VAZLO +127,500, LUK +477, RAYBESTOS +1,743 - Added KNADIAN catalog importer with year-range expansion and future-year filtering - Added VAZLO catalog importer with position parsing and SKU-in-model cleanup - Added Keep Green, LUK, Yokomitsu, Raybestos catalog importers - Cache clearing after cleanups (_classify_cache_*, nexus:mye_ids:*, nexus:brand_mye_counts:*) Final match rates: - KEEP GREEN: 90.3% - VAZLO: 93.6% - YOKOMITSU: 100.0% - KNADIAN: 57.4% - LUK: 51.0% - RAYBESTOS: 55.9%
169 lines
5.0 KiB
Python
169 lines
5.0 KiB
Python
"""Dropshipping integration service.
|
|
|
|
Provides read-only inventory access for external dropshipping platforms
|
|
and webhook dispatching on stock/price/sale events.
|
|
"""
|
|
|
|
import logging
|
|
from services.inventory_engine import get_stock_bulk
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def resolve_tenant_by_api_key(master_conn, api_key: str):
|
|
"""Find tenant_id and db_name for a given dropshipping API key.
|
|
|
|
Returns (tenant_id, db_name) or (None, None) if invalid.
|
|
"""
|
|
if not api_key:
|
|
return None, None
|
|
cur = master_conn.cursor()
|
|
# tenant_config lives in each tenant DB, so we need to scan tenants
|
|
cur.execute("SELECT id, db_name FROM tenants WHERE is_active = true")
|
|
tenants = cur.fetchall()
|
|
for tid, db_name in tenants:
|
|
try:
|
|
tcur = master_conn.cursor()
|
|
# Use dblink or connect to tenant DB? Simpler: the blueprint
|
|
# will pass tenant_conn directly after resolution.
|
|
# Instead, we store a mapping in master DB for speed.
|
|
# For now, return all candidates and let caller validate.
|
|
pass
|
|
except Exception:
|
|
continue
|
|
cur.close()
|
|
return None, None
|
|
|
|
|
|
def _get_dropshipping_key(tenant_conn):
|
|
cur = tenant_conn.cursor()
|
|
cur.execute("SELECT value FROM tenant_config WHERE key = 'dropshipping_api_key'")
|
|
row = cur.fetchone()
|
|
cur.close()
|
|
return row[0] if row else None
|
|
|
|
|
|
def validate_api_key(tenant_conn, api_key: str) -> bool:
|
|
"""Check if the provided API key matches the tenant's configured key."""
|
|
if not api_key:
|
|
return False
|
|
expected = _get_dropshipping_key(tenant_conn)
|
|
return expected is not None and expected == api_key
|
|
|
|
|
|
def get_inventory_list(tenant_conn, search: str = None, page: int = 1, per_page: int = 50):
|
|
"""Return inventory items with stock and price for dropshipping."""
|
|
offset = (max(page, 1) - 1) * per_page
|
|
stock_map = get_stock_bulk(tenant_conn, branch_id=None)
|
|
|
|
cur = tenant_conn.cursor()
|
|
params = []
|
|
where = "WHERE is_active = true"
|
|
if search:
|
|
where += " AND (name ILIKE %s OR part_number ILIKE %s)"
|
|
params.extend([f"%{search}%", f"%{search}%"])
|
|
|
|
cur.execute(
|
|
f"""
|
|
SELECT id, part_number, name, brand, price_1, price_2, price_3,
|
|
image_url, unit, description
|
|
FROM inventory
|
|
{where}
|
|
ORDER BY id DESC
|
|
LIMIT %s OFFSET %s
|
|
""",
|
|
params + [per_page, offset],
|
|
)
|
|
rows = cur.fetchall()
|
|
|
|
# Count total
|
|
cur.execute(f"SELECT COUNT(*) FROM inventory {where}", params)
|
|
total = cur.fetchone()[0]
|
|
cur.close()
|
|
|
|
items = []
|
|
for r in rows:
|
|
inv_id = r[0]
|
|
items.append({
|
|
"id": inv_id,
|
|
"sku": r[1],
|
|
"name": r[2],
|
|
"brand": r[3],
|
|
"price_1": float(r[4]) if r[4] else None,
|
|
"price_2": float(r[5]) if r[5] else None,
|
|
"price_3": float(r[6]) if r[6] else None,
|
|
"stock": stock_map.get(inv_id, 0),
|
|
"image_url": r[7],
|
|
"unit": r[8],
|
|
"description": r[9],
|
|
})
|
|
return {"items": items, "page": page, "per_page": per_page, "total": total}
|
|
|
|
|
|
def get_inventory_by_sku(tenant_conn, sku: str):
|
|
"""Return a single inventory item by SKU/part_number."""
|
|
stock_map = get_stock_bulk(tenant_conn, branch_id=None)
|
|
cur = tenant_conn.cursor()
|
|
cur.execute(
|
|
"""
|
|
SELECT id, part_number, name, brand, price_1, price_2, price_3,
|
|
image_url, unit, description
|
|
FROM inventory
|
|
WHERE part_number = %s AND is_active = true
|
|
LIMIT 1
|
|
""",
|
|
(sku,),
|
|
)
|
|
row = cur.fetchone()
|
|
cur.close()
|
|
if not row:
|
|
return None
|
|
inv_id = row[0]
|
|
return {
|
|
"id": inv_id,
|
|
"sku": row[1],
|
|
"name": row[2],
|
|
"brand": row[3],
|
|
"price_1": float(row[4]) if row[4] else None,
|
|
"price_2": float(row[5]) if row[5] else None,
|
|
"price_3": float(row[6]) if row[6] else None,
|
|
"stock": stock_map.get(inv_id, 0),
|
|
"image_url": row[7],
|
|
"unit": row[8],
|
|
"description": row[9],
|
|
}
|
|
|
|
|
|
def get_stock_by_skus(tenant_conn, skus: list[str]) -> dict:
|
|
"""Return stock levels for a list of SKUs."""
|
|
stock_map = get_stock_bulk(tenant_conn, branch_id=None)
|
|
cur = tenant_conn.cursor()
|
|
cur.execute(
|
|
"""
|
|
SELECT id, part_number FROM inventory
|
|
WHERE part_number = ANY(%s) AND is_active = true
|
|
""",
|
|
(skus,),
|
|
)
|
|
rows = cur.fetchall()
|
|
cur.close()
|
|
result = {}
|
|
for inv_id, sku in rows:
|
|
result[sku] = stock_map.get(inv_id, 0)
|
|
return result
|
|
|
|
|
|
def get_webhook_targets(tenant_conn, event_type: str) -> list[str]:
|
|
"""Return active webhook URLs for a given event type."""
|
|
cur = tenant_conn.cursor()
|
|
cur.execute(
|
|
"""
|
|
SELECT target_url FROM dropshipping_webhooks
|
|
WHERE event_type = %s AND is_active = true
|
|
""",
|
|
(event_type,),
|
|
)
|
|
urls = [r[0] for r in cur.fetchall()]
|
|
cur.close()
|
|
return urls
|