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:
128
pos/blueprints/dropshipping_bp.py
Normal file
128
pos/blueprints/dropshipping_bp.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""Dropshipping API — public read-only inventory endpoints.
|
||||
|
||||
Authentication: X-Dropshipping-Key header (per-tenant).
|
||||
Optional: X-Tenant-Subdomain for faster resolution.
|
||||
"""
|
||||
|
||||
from flask import Blueprint, request, jsonify, g
|
||||
from tenant_db import get_tenant_conn, get_master_conn
|
||||
from services import dropshipping_service as ds_svc
|
||||
from services.webhook_service import dispatch_webhooks_bulk
|
||||
|
||||
dropship_bp = Blueprint("dropship", __name__, url_prefix="/pos/api/dropship")
|
||||
|
||||
|
||||
def _resolve_tenant_by_key(api_key: str, subdomain_hint: str = None):
|
||||
"""Return (tenant_conn, tenant_id) for a valid dropshipping API key.
|
||||
|
||||
If subdomain_hint is provided, validate only that tenant.
|
||||
Otherwise scan active tenants (acceptable for small tenant count).
|
||||
"""
|
||||
master = get_master_conn()
|
||||
try:
|
||||
cur = master.cursor()
|
||||
if subdomain_hint:
|
||||
cur.execute(
|
||||
"SELECT id, db_name FROM tenants WHERE subdomain = %s AND is_active = true",
|
||||
(subdomain_hint,),
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
else:
|
||||
cur.execute("SELECT id, db_name FROM tenants WHERE is_active = true")
|
||||
rows = cur.fetchall()
|
||||
cur.close()
|
||||
|
||||
for tid, db_name in rows:
|
||||
try:
|
||||
tconn = get_tenant_conn(tid)
|
||||
if ds_svc.validate_api_key(tconn, api_key):
|
||||
return tconn, tid
|
||||
tconn.close()
|
||||
except Exception:
|
||||
continue
|
||||
return None, None
|
||||
finally:
|
||||
master.close()
|
||||
|
||||
|
||||
def _require_dropship_auth():
|
||||
key = request.headers.get("X-Dropshipping-Key")
|
||||
subdomain = request.headers.get("X-Tenant-Subdomain")
|
||||
if not key:
|
||||
return jsonify({"error": "Missing X-Dropshipping-Key header"}), 401
|
||||
tconn, tid = _resolve_tenant_by_key(key, subdomain_hint=subdomain)
|
||||
if not tconn:
|
||||
return jsonify({"error": "Invalid API key or tenant inactive"}), 401
|
||||
g.tenant_id = tid
|
||||
g.tenant_conn = tconn
|
||||
return None
|
||||
|
||||
|
||||
def _release_tenant():
|
||||
if hasattr(g, "tenant_conn") and g.tenant_conn:
|
||||
g.tenant_conn.close()
|
||||
|
||||
|
||||
@dropship_bp.route("/inventory", methods=["GET"])
|
||||
def list_inventory():
|
||||
err = _require_dropship_auth()
|
||||
if err:
|
||||
return err
|
||||
try:
|
||||
page = int(request.args.get("page", 1))
|
||||
per_page = min(int(request.args.get("per_page", 50)), 200)
|
||||
search = request.args.get("q")
|
||||
result = ds_svc.get_inventory_list(g.tenant_conn, search=search, page=page, per_page=per_page)
|
||||
return jsonify(result)
|
||||
finally:
|
||||
_release_tenant()
|
||||
|
||||
|
||||
@dropship_bp.route("/inventory/<sku>", methods=["GET"])
|
||||
def get_inventory_item(sku):
|
||||
err = _require_dropship_auth()
|
||||
if err:
|
||||
return err
|
||||
try:
|
||||
item = ds_svc.get_inventory_by_sku(g.tenant_conn, sku)
|
||||
if not item:
|
||||
return jsonify({"error": "SKU not found"}), 404
|
||||
return jsonify(item)
|
||||
finally:
|
||||
_release_tenant()
|
||||
|
||||
|
||||
@dropship_bp.route("/stock", methods=["GET"])
|
||||
def get_stock():
|
||||
err = _require_dropship_auth()
|
||||
if err:
|
||||
return err
|
||||
try:
|
||||
skus = request.args.get("skus", "")
|
||||
sku_list = [s.strip() for s in skus.split(",") if s.strip()]
|
||||
if not sku_list:
|
||||
return jsonify({"error": "Provide ?skus=SKU1,SKU2,SKU3"}), 400
|
||||
result = ds_svc.get_stock_by_skus(g.tenant_conn, sku_list)
|
||||
return jsonify({"stock": result})
|
||||
finally:
|
||||
_release_tenant()
|
||||
|
||||
|
||||
@dropship_bp.route("/webhooks/test", methods=["POST"])
|
||||
def test_webhook():
|
||||
"""Test endpoint to trigger a sample webhook to all configured targets."""
|
||||
err = _require_dropship_auth()
|
||||
if err:
|
||||
return err
|
||||
try:
|
||||
urls = ds_svc.get_webhook_targets(g.tenant_conn, "stock_updated")
|
||||
if not urls:
|
||||
return jsonify({"error": "No webhook targets configured"}), 400
|
||||
results = dispatch_webhooks_bulk(
|
||||
urls,
|
||||
"test",
|
||||
{"message": "Webhook test from Nexus POS", "tenant_id": g.tenant_id},
|
||||
)
|
||||
return jsonify({"dispatched": len(results), "results": results})
|
||||
finally:
|
||||
_release_tenant()
|
||||
Reference in New Issue
Block a user