- 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%
644 lines
22 KiB
Python
644 lines
22 KiB
Python
"""MercadoLibre external marketplace REST endpoints.
|
|
|
|
Routes:
|
|
Config
|
|
GET /pos/api/marketplace-ext/config
|
|
POST /pos/api/marketplace-ext/connect
|
|
DELETE /pos/api/marketplace-ext/connect
|
|
GET /pos/api/marketplace-ext/categories
|
|
|
|
Listings
|
|
GET /pos/api/marketplace-ext/listings
|
|
POST /pos/api/marketplace-ext/listings
|
|
POST /pos/api/marketplace-ext/listings/<id>/sync
|
|
POST /pos/api/marketplace-ext/listings/<id>/pause
|
|
POST /pos/api/marketplace-ext/listings/<id>/activate
|
|
DELETE /pos/api/marketplace-ext/listings/<id>
|
|
|
|
Orders
|
|
GET /pos/api/marketplace-ext/orders
|
|
GET /pos/api/marketplace-ext/orders/<id>
|
|
POST /pos/api/marketplace-ext/orders/<id>/convert
|
|
|
|
Webhook (public)
|
|
POST /pos/api/marketplace-ext/webhook/meli
|
|
"""
|
|
|
|
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(
|
|
"marketplace_ext", __name__, url_prefix="/pos/api/marketplace-ext"
|
|
)
|
|
|
|
|
|
# ─── Helpers ───────────────────────────────────────────────────────────────
|
|
|
|
def _require_meli_manage():
|
|
if not has_permission("marketplace.manage"):
|
|
return jsonify({"error": "Missing permission: marketplace.manage"}), 403
|
|
return None
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# CONFIG
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
@marketplace_ext_bp.route("/config", methods=["GET"])
|
|
@require_auth()
|
|
def get_config():
|
|
conn = get_tenant_conn(g.tenant_id)
|
|
try:
|
|
cfg = meli_svc.get_meli_config(conn)
|
|
# Never return tokens to frontend
|
|
safe = {
|
|
k: v for k, v in cfg.items()
|
|
if k not in ("meli_access_token", "meli_refresh_token", "meli_client_secret")
|
|
}
|
|
safe["connected"] = bool(cfg.get("meli_access_token"))
|
|
return jsonify(safe)
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
@marketplace_ext_bp.route("/connect", methods=["POST"])
|
|
@require_auth()
|
|
def connect_meli():
|
|
err = _require_meli_manage()
|
|
if err:
|
|
return err
|
|
|
|
data = request.get_json() or {}
|
|
code = data.get("code")
|
|
client_id = data.get("client_id")
|
|
client_secret = data.get("client_secret")
|
|
redirect_uri = data.get("redirect_uri", "")
|
|
|
|
if not code or not client_id or not client_secret:
|
|
return jsonify({"error": "code, client_id and client_secret required"}), 400
|
|
|
|
try:
|
|
token_data = MeliService.exchange_code(code, client_id, client_secret, redirect_uri)
|
|
except MeliAuthError as e:
|
|
return jsonify({"error": str(e)}), 400
|
|
|
|
access_token = token_data.get("access_token")
|
|
refresh_token = token_data.get("refresh_token")
|
|
user_id = token_data.get("user_id")
|
|
|
|
# Validate token by fetching user
|
|
svc = MeliService(access_token)
|
|
try:
|
|
user = svc.get_user()
|
|
except MeliAuthError as e:
|
|
return jsonify({"error": f"Invalid token: {e}"}), 400
|
|
|
|
conn = get_tenant_conn(g.tenant_id)
|
|
try:
|
|
meli_svc.save_meli_config(conn, {
|
|
"meli_access_token": access_token,
|
|
"meli_refresh_token": refresh_token,
|
|
"meli_user_id": str(user_id or user.get("id")),
|
|
"meli_site_id": user.get("site_id", "MLM"),
|
|
"meli_enabled": "true",
|
|
"meli_client_id": client_id,
|
|
"meli_client_secret": client_secret,
|
|
})
|
|
return jsonify({
|
|
"ok": True,
|
|
"user_id": user_id or user.get("id"),
|
|
"nickname": user.get("nickname"),
|
|
"site_id": user.get("site_id"),
|
|
})
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
@marketplace_ext_bp.route("/connect", methods=["DELETE"])
|
|
@require_auth()
|
|
def disconnect_meli():
|
|
err = _require_meli_manage()
|
|
if err:
|
|
return err
|
|
|
|
conn = get_tenant_conn(g.tenant_id)
|
|
try:
|
|
meli_svc.delete_meli_config(conn)
|
|
return jsonify({"ok": True})
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
@marketplace_ext_bp.route("/categories", methods=["GET"])
|
|
@require_auth()
|
|
def search_categories():
|
|
q = request.args.get("q", "")
|
|
site_id = request.args.get("site_id", "MLM")
|
|
if not q or len(q) < 2:
|
|
return jsonify({"categories": []})
|
|
|
|
conn = get_tenant_conn(g.tenant_id)
|
|
try:
|
|
cfg = meli_svc.get_meli_config(conn)
|
|
svc = meli_svc._get_meli_service(cfg)
|
|
if not svc:
|
|
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()
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# LISTINGS
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
@marketplace_ext_bp.route("/listings", methods=["GET"])
|
|
@require_auth()
|
|
def list_listings():
|
|
page = int(request.args.get("page", 1))
|
|
per_page = min(int(request.args.get("per_page", 50)), 200)
|
|
status = request.args.get("status")
|
|
|
|
conn = get_tenant_conn(g.tenant_id)
|
|
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()
|
|
|
|
|
|
@marketplace_ext_bp.route("/listings", methods=["POST"])
|
|
@require_auth()
|
|
def create_listings():
|
|
err = _require_meli_manage()
|
|
if err:
|
|
return err
|
|
|
|
data = request.get_json() or {}
|
|
inventory_ids = data.get("inventory_ids", [])
|
|
category_id = data.get("category_id")
|
|
listing_type = data.get("listing_type", "gold_special")
|
|
shipping_mode = data.get("shipping_mode", "me2")
|
|
custom_data = data.get("custom_data", {})
|
|
|
|
if not inventory_ids:
|
|
return jsonify({"error": "inventory_ids required"}), 400
|
|
if not category_id:
|
|
return jsonify({"error": "category_id required"}), 400
|
|
|
|
conn = get_tenant_conn(g.tenant_id)
|
|
try:
|
|
result = meli_svc.publish_items(
|
|
conn,
|
|
inventory_ids=inventory_ids,
|
|
meli_category_id=category_id,
|
|
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:
|
|
return jsonify({"error": str(e)}), 400
|
|
except Exception as e:
|
|
return jsonify({"error": str(e)}), 500
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
@marketplace_ext_bp.route("/inventory-check", methods=["POST"])
|
|
@require_auth()
|
|
def inventory_check():
|
|
"""Check local pre-flight status for ML publishing (duplicates, stock, price, image)."""
|
|
data = request.get_json() or {}
|
|
inventory_ids = data.get("inventory_ids", [])
|
|
if not inventory_ids:
|
|
return jsonify({"error": "inventory_ids required"}), 400
|
|
|
|
conn = get_tenant_conn(g.tenant_id)
|
|
try:
|
|
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
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
@marketplace_ext_bp.route("/categories/<category_id>/attributes", methods=["GET"])
|
|
@require_auth()
|
|
def category_attributes(category_id):
|
|
"""Get required attributes for a MercadoLibre category."""
|
|
conn = get_tenant_conn(g.tenant_id)
|
|
try:
|
|
cfg = meli_svc.get_meli_config(conn)
|
|
svc = meli_svc._get_meli_service(cfg)
|
|
if not svc:
|
|
return jsonify({"error": "MercadoLibre not connected"}), 400
|
|
attrs = svc.get_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:
|
|
conn.close()
|
|
|
|
|
|
@marketplace_ext_bp.route("/listings/validate", methods=["POST"])
|
|
@require_auth()
|
|
def validate_listings():
|
|
"""Validate items payload against ML /items/validate without creating them."""
|
|
err = _require_meli_manage()
|
|
if err:
|
|
return err
|
|
|
|
data = request.get_json() or {}
|
|
inventory_ids = data.get("inventory_ids", [])
|
|
category_id = data.get("category_id")
|
|
listing_type = data.get("listing_type", "gold_special")
|
|
shipping_mode = data.get("shipping_mode", "me2")
|
|
custom_data = data.get("custom_data", {})
|
|
|
|
if not inventory_ids:
|
|
return jsonify({"error": "inventory_ids required"}), 400
|
|
if not category_id:
|
|
return jsonify({"error": "category_id required"}), 400
|
|
|
|
conn = get_tenant_conn(g.tenant_id)
|
|
try:
|
|
result = meli_svc.validate_items(
|
|
conn,
|
|
inventory_ids=inventory_ids,
|
|
meli_category_id=category_id,
|
|
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:
|
|
return jsonify({"error": str(e)}), 400
|
|
except Exception as e:
|
|
return jsonify({"error": str(e)}), 500
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
@marketplace_ext_bp.route("/listings/async", methods=["POST"])
|
|
@require_auth()
|
|
def create_listings_async():
|
|
"""Enqueue ML publishing as a Celery background task."""
|
|
err = _require_meli_manage()
|
|
if err:
|
|
return err
|
|
|
|
data = request.get_json() or {}
|
|
inventory_ids = data.get("inventory_ids", [])
|
|
category_id = data.get("category_id")
|
|
listing_type = data.get("listing_type", "gold_special")
|
|
shipping_mode = data.get("shipping_mode", "me2")
|
|
custom_data = data.get("custom_data", {})
|
|
|
|
if not inventory_ids:
|
|
return jsonify({"error": "inventory_ids required"}), 400
|
|
if not category_id:
|
|
return jsonify({"error": "category_id required"}), 400
|
|
|
|
try:
|
|
from tasks import publish_meli_items_task
|
|
task = publish_meli_items_task.delay(
|
|
g.tenant_id,
|
|
inventory_ids=inventory_ids,
|
|
category_id=category_id,
|
|
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:
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
|
|
@marketplace_ext_bp.route("/listings/async/<task_id>", methods=["GET"])
|
|
@require_auth()
|
|
def get_async_listing_status(task_id):
|
|
"""Get status of an async ML publishing task."""
|
|
try:
|
|
from celery.result import AsyncResult
|
|
from app import celery as celery_app
|
|
result = AsyncResult(task_id, app=celery_app)
|
|
if result.ready():
|
|
return jsonify({"status": "done", "result": result.result or {}})
|
|
return jsonify({"status": "pending"})
|
|
except Exception as e:
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
|
|
@marketplace_ext_bp.route("/listings/<int:listing_id>/sync", methods=["POST"])
|
|
@require_auth()
|
|
def sync_listing(listing_id):
|
|
err = _require_meli_manage()
|
|
if err:
|
|
return err
|
|
|
|
conn = get_tenant_conn(g.tenant_id)
|
|
try:
|
|
result = meli_svc.sync_listing(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()
|
|
|
|
|
|
@marketplace_ext_bp.route("/listings/<int:listing_id>/pause", methods=["POST"])
|
|
@require_auth()
|
|
def pause_listing(listing_id):
|
|
err = _require_meli_manage()
|
|
if err:
|
|
return err
|
|
|
|
conn = get_tenant_conn(g.tenant_id)
|
|
try:
|
|
result = meli_svc.pause_listing(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()
|
|
|
|
|
|
@marketplace_ext_bp.route("/listings/<int:listing_id>/activate", methods=["POST"])
|
|
@require_auth()
|
|
def activate_listing(listing_id):
|
|
err = _require_meli_manage()
|
|
if err:
|
|
return err
|
|
|
|
conn = get_tenant_conn(g.tenant_id)
|
|
try:
|
|
result = meli_svc.activate_listing(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()
|
|
|
|
|
|
@marketplace_ext_bp.route("/listings/<int:listing_id>", methods=["DELETE"])
|
|
@require_auth()
|
|
def delete_listing(listing_id):
|
|
err = _require_meli_manage()
|
|
if err:
|
|
return err
|
|
|
|
conn = get_tenant_conn(g.tenant_id)
|
|
try:
|
|
result = meli_svc.close_listing(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()
|
|
|
|
|
|
@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
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
@marketplace_ext_bp.route("/orders", methods=["GET"])
|
|
@require_auth()
|
|
def list_orders():
|
|
page = int(request.args.get("page", 1))
|
|
per_page = min(int(request.args.get("per_page", 50)), 200)
|
|
status = request.args.get("status")
|
|
|
|
conn = get_tenant_conn(g.tenant_id)
|
|
try:
|
|
result = meli_svc.get_orders(conn, page=page, per_page=per_page, status=status)
|
|
return jsonify(result)
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
@marketplace_ext_bp.route("/orders/<int:order_id>", methods=["GET"])
|
|
@require_auth()
|
|
def get_order(order_id):
|
|
conn = get_tenant_conn(g.tenant_id)
|
|
try:
|
|
result = meli_svc.get_order_detail(conn, order_id)
|
|
return jsonify(result)
|
|
except ValueError as e:
|
|
return jsonify({"error": str(e)}), 404
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
@marketplace_ext_bp.route("/orders/<int:order_id>/convert", methods=["POST"])
|
|
@require_auth("pos.sell")
|
|
def convert_order(order_id):
|
|
data = request.get_json() or {}
|
|
register_id = data.get("register_id")
|
|
|
|
conn = get_tenant_conn(g.tenant_id)
|
|
try:
|
|
result = meli_svc.convert_order_to_sale(
|
|
conn, order_id, employee_id=g.employee_id, register_id=register_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()
|
|
|
|
|
|
@marketplace_ext_bp.route("/orders/<int:order_id>/status", methods=["POST"])
|
|
@require_auth()
|
|
def update_order_status_route(order_id):
|
|
data = request.get_json() or {}
|
|
new_status = data.get("status")
|
|
if not new_status:
|
|
return jsonify({"error": "status required"}), 400
|
|
|
|
conn = get_tenant_conn(g.tenant_id)
|
|
try:
|
|
result = meli_svc.update_order_status(conn, order_id, new_status)
|
|
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()
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# WEBHOOK (public — no auth)
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
@marketplace_ext_bp.route("/webhook/meli", methods=["POST"])
|
|
def meli_webhook():
|
|
"""Receive MercadoLibre notifications.
|
|
|
|
ML sends a lightweight payload with topic + resource URL.
|
|
We ack immediately and enqueue Celery for async processing.
|
|
"""
|
|
data = request.get_json(force=True, silent=True) or {}
|
|
topic = data.get("topic", "")
|
|
resource = data.get("resource", "")
|
|
user_id = data.get("user_id")
|
|
|
|
# Resolve tenant by meli_user_id
|
|
tenant_id = None
|
|
if user_id:
|
|
try:
|
|
mconn = get_master_conn()
|
|
mcur = mconn.cursor()
|
|
mcur.execute(
|
|
"""
|
|
SELECT t.id FROM tenants t
|
|
JOIN tenant_config c ON c.key = 'meli_user_id' AND c.value = %s
|
|
WHERE t.is_active = true
|
|
LIMIT 1
|
|
""",
|
|
(str(user_id),),
|
|
)
|
|
row = mcur.fetchone()
|
|
if row:
|
|
tenant_id = row[0]
|
|
mcur.close()
|
|
mconn.close()
|
|
except Exception:
|
|
pass
|
|
|
|
if tenant_id and topic:
|
|
try:
|
|
from tasks import process_meli_webhook_task
|
|
process_meli_webhook_task.delay(tenant_id, topic, resource)
|
|
except Exception as e:
|
|
print(f"[ML Webhook] Failed to enqueue task: {e}")
|
|
|
|
return jsonify({"ok": True})
|