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:
@@ -28,6 +28,23 @@ from flask import Blueprint, request, jsonify, g
|
||||
from middleware import require_auth, has_permission
|
||||
from tenant_db import get_tenant_conn, get_master_conn
|
||||
from services import marketplace_external_service as meli_svc
|
||||
|
||||
|
||||
def _get_public_base_url() -> str:
|
||||
"""Build the tenant's public base URL from request headers (handles reverse proxy)."""
|
||||
proto = request.headers.get("X-Forwarded-Proto", request.scheme)
|
||||
host = request.headers.get("X-Forwarded-Host", request.host)
|
||||
|
||||
# Cloudflare specific header
|
||||
cf_visitor = request.headers.get("CF-Visitor")
|
||||
if cf_visitor and '"scheme":"https"' in cf_visitor:
|
||||
proto = "https"
|
||||
|
||||
# Force https for production domain if we detect http behind a TLS terminator
|
||||
if proto == "http" and ("nexusautoparts.com.mx" in host or request.headers.get("X-Forwarded-Ssl") == "on"):
|
||||
proto = "https"
|
||||
|
||||
return f"{proto}://{host}/"
|
||||
from services.meli_service import MeliService, MeliAuthError
|
||||
|
||||
marketplace_ext_bp = Blueprint(
|
||||
@@ -148,6 +165,10 @@ def search_categories():
|
||||
return jsonify({"error": "MercadoLibre not connected"}), 400
|
||||
result = svc.search_categories(site_id, q)
|
||||
return jsonify({"categories": result})
|
||||
except MeliAuthError:
|
||||
return jsonify({"error": "MercadoLibre token expired. Please reconnect."}), 401
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@@ -167,6 +188,10 @@ def list_listings():
|
||||
try:
|
||||
result = meli_svc.get_listings(conn, page=page, per_page=per_page, status=status)
|
||||
return jsonify(result)
|
||||
except MeliAuthError:
|
||||
return jsonify({"error": "MercadoLibre token expired. Please reconnect."}), 401
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@@ -199,6 +224,7 @@ def create_listings():
|
||||
listing_type_id=listing_type,
|
||||
shipping_mode=shipping_mode,
|
||||
custom_data=custom_data,
|
||||
base_url=_get_public_base_url(),
|
||||
)
|
||||
return jsonify(result), 201
|
||||
except ValueError as e:
|
||||
@@ -220,7 +246,7 @@ def inventory_check():
|
||||
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
try:
|
||||
result = meli_svc.check_inventory_ml_status(conn, inventory_ids)
|
||||
result = meli_svc.check_inventory_ml_status(conn, inventory_ids, base_url=_get_public_base_url())
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
@@ -242,6 +268,8 @@ def category_attributes(category_id):
|
||||
# Filter to required attributes only for the UI
|
||||
required = [a for a in attrs if a.get("tags", {}).get("required")]
|
||||
return jsonify({"attributes": required, "all": attrs})
|
||||
except MeliAuthError:
|
||||
return jsonify({"error": "MercadoLibre token expired. Please reconnect."}), 401
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
finally:
|
||||
@@ -277,6 +305,7 @@ def validate_listings():
|
||||
listing_type_id=listing_type,
|
||||
shipping_mode=shipping_mode,
|
||||
custom_data=custom_data,
|
||||
base_url=_get_public_base_url(),
|
||||
)
|
||||
return jsonify(result)
|
||||
except ValueError as e:
|
||||
@@ -316,6 +345,7 @@ def create_listings_async():
|
||||
listing_type=listing_type,
|
||||
shipping_mode=shipping_mode,
|
||||
custom_data=custom_data,
|
||||
base_url=_get_public_base_url(),
|
||||
)
|
||||
return jsonify({"task_id": task.id, "status": "queued"}), 202
|
||||
except Exception as e:
|
||||
@@ -413,6 +443,85 @@ def delete_listing(listing_id):
|
||||
conn.close()
|
||||
|
||||
|
||||
@marketplace_ext_bp.route("/listings/<int:listing_id>/permanent", methods=["DELETE"])
|
||||
@require_auth()
|
||||
def delete_listing_permanent(listing_id):
|
||||
"""Hard-delete a closed listing from the local DB."""
|
||||
err = _require_meli_manage()
|
||||
if err:
|
||||
return err
|
||||
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
try:
|
||||
result = meli_svc.delete_listing_permanently(conn, listing_id)
|
||||
return jsonify(result)
|
||||
except ValueError as e:
|
||||
return jsonify({"error": str(e)}), 400
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# QUESTIONS & ANSWERS
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@marketplace_ext_bp.route("/questions", methods=["GET"])
|
||||
@require_auth()
|
||||
def list_questions():
|
||||
"""List questions from local DB. Query param: ?status=unanswered"""
|
||||
status = request.args.get("status")
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
try:
|
||||
items = meli_svc.list_local_questions(conn, status=status)
|
||||
return jsonify({"items": items})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@marketplace_ext_bp.route("/questions/sync", methods=["POST"])
|
||||
@require_auth()
|
||||
def sync_questions():
|
||||
"""Force sync questions from ML for all active listings."""
|
||||
err = _require_meli_manage()
|
||||
if err:
|
||||
return err
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
try:
|
||||
result = meli_svc.sync_questions(conn)
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@marketplace_ext_bp.route("/questions/<int:question_id>/answer", methods=["POST"])
|
||||
@require_auth()
|
||||
def answer_question(question_id):
|
||||
"""Answer a buyer question via ML API."""
|
||||
err = _require_meli_manage()
|
||||
if err:
|
||||
return err
|
||||
data = request.get_json() or {}
|
||||
text = data.get("text", "").strip()
|
||||
if not text:
|
||||
return jsonify({"error": "Answer text is required"}), 400
|
||||
conn = get_tenant_conn(g.tenant_id)
|
||||
try:
|
||||
result = meli_svc.answer_question(conn, question_id, text)
|
||||
return jsonify(result)
|
||||
except ValueError as e:
|
||||
return jsonify({"error": str(e)}), 400
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# ORDERS
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user