- 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%
129 lines
4.1 KiB
Python
129 lines
4.1 KiB
Python
"""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()
|