feat(catalog): supplier catalog cleanup, fuzzy matching, and navigation fixes
- 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%
This commit is contained in:
168
pos/services/dropshipping_service.py
Normal file
168
pos/services/dropshipping_service.py
Normal file
@@ -0,0 +1,168 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user