feat: MercadoLibre integration + inventory bulk publish + WhatsApp bridge fixes
- Add MercadoLibre OAuth, listings, orders, webhooks and category search - New marketplace_external_bp.py, meli_service.py, marketplace_external_service.py - New marketplace_external.html/js with ML management UI - Inventory: bulk publish to ML with category autocomplete, listing type and shipping selectors - Inventory: new .btn--meli styles, select/label CSS fixes - WhatsApp bridge: rate limiting, 440/515/408 error handling, stale watchdog - DB migration v3.4_meli_integration.sql for marketplace_listings, orders, sync_queue - Add Celery tasks for ML sync and webhook processing - Sidebar: MercadoLibre navigation link
This commit is contained in:
127
pos/tasks.py
127
pos/tasks.py
@@ -98,6 +98,133 @@ def generate_report_task(self, report_type, params, tenant_id):
|
||||
}
|
||||
|
||||
|
||||
# ─── MercadoLibre Tasks ───────────────────────────────────────────────────
|
||||
|
||||
@celery.task(bind=True, max_retries=3)
|
||||
def sync_meli_stock_price_task(self, tenant_id: int):
|
||||
"""Sync stock and price for all active ML listings."""
|
||||
from services import marketplace_external_service as meli_svc
|
||||
from tenant_db import get_tenant_conn
|
||||
|
||||
conn = get_tenant_conn(tenant_id)
|
||||
try:
|
||||
cfg = meli_svc.get_meli_config(conn)
|
||||
svc = meli_svc._get_meli_service(cfg)
|
||||
if not svc:
|
||||
return {'error': 'MercadoLibre not configured'}
|
||||
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, inventory_id, external_item_id, publish_price
|
||||
FROM marketplace_listings
|
||||
WHERE is_active = true AND channel = 'mercadolibre'
|
||||
"""
|
||||
)
|
||||
listings = cur.fetchall()
|
||||
cur.close()
|
||||
|
||||
from services.inventory_engine import get_stock_bulk
|
||||
stock_map = get_stock_bulk(conn, branch_id=None)
|
||||
|
||||
updated = 0
|
||||
failed = 0
|
||||
for row in listings:
|
||||
listing_id, inv_id, ext_id, last_price = row
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT price_1 FROM inventory WHERE id = %s", (inv_id,))
|
||||
price_row = cur.fetchone()
|
||||
cur.close()
|
||||
current_price = float(price_row[0]) if price_row and price_row[0] else 0
|
||||
current_stock = stock_map.get(inv_id, 0)
|
||||
|
||||
try:
|
||||
svc.update_item(
|
||||
ext_id,
|
||||
{"price": round(current_price, 2), "available_quantity": max(current_stock, 0)},
|
||||
)
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE marketplace_listings
|
||||
SET last_sync_at = NOW(), sync_errors = NULL, publish_price = %s
|
||||
WHERE id = %s
|
||||
""",
|
||||
(current_price, listing_id),
|
||||
)
|
||||
conn.commit()
|
||||
cur.close()
|
||||
updated += 1
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"UPDATE marketplace_listings SET sync_errors = %s WHERE id = %s",
|
||||
(str(e)[:500], listing_id),
|
||||
)
|
||||
conn.commit()
|
||||
cur.close()
|
||||
failed += 1
|
||||
|
||||
return {'updated': updated, 'failed': failed, 'total': len(listings)}
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@celery.task(bind=True, max_retries=3)
|
||||
def sync_meli_orders_task(self, tenant_id: int):
|
||||
"""Fetch new orders from MercadoLibre."""
|
||||
from services import marketplace_external_service as meli_svc
|
||||
from tenant_db import get_tenant_conn
|
||||
|
||||
conn = get_tenant_conn(tenant_id)
|
||||
try:
|
||||
# Determine last check date from most recent order
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"SELECT MAX(created_at) FROM marketplace_orders WHERE channel = 'mercadolibre'"
|
||||
)
|
||||
row = cur.fetchone()
|
||||
last_check = row[0]
|
||||
cur.close()
|
||||
|
||||
date_from = None
|
||||
if last_check:
|
||||
date_from = last_check.strftime('%Y-%m-%dT%H:%M:%S.000-00:00')
|
||||
|
||||
result = meli_svc.fetch_and_save_orders(conn, date_from=date_from)
|
||||
return result
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@celery.task(bind=True, max_retries=3)
|
||||
def process_meli_webhook_task(self, tenant_id: int, topic: str, resource: str):
|
||||
"""Process incoming MercadoLibre webhook asynchronously."""
|
||||
from services import marketplace_external_service as meli_svc
|
||||
from tenant_db import get_tenant_conn
|
||||
|
||||
conn = get_tenant_conn(tenant_id)
|
||||
try:
|
||||
if topic.startswith("orders"):
|
||||
# Fetch full order and upsert
|
||||
cfg = meli_svc.get_meli_config(conn)
|
||||
svc = meli_svc._get_meli_service(cfg)
|
||||
if svc and resource:
|
||||
order_id = resource.split("/")[-1]
|
||||
full = svc.get_order(order_id)
|
||||
# Re-use fetch_and_save_orders by passing the order directly
|
||||
# For simplicity, trigger a full sync for recent orders
|
||||
return meli_svc.fetch_and_save_orders(conn)
|
||||
return {'ok': True, 'topic': topic}
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@celery.task(bind=True, max_retries=2)
|
||||
def sync_vehicle_compatibility_task(self, tenant_id, item_id, part_number, name, brand, compat_source):
|
||||
"""Fetch AI/TecDoc vehicle compatibility in background after item creation."""
|
||||
|
||||
Reference in New Issue
Block a user