feat(ml): upload images to ML hosting + show earnings estimate on validate
- Add upload_image() to MeliService using ML /pictures endpoint - Add get_listing_price() to fetch exact ML fees - Auto-upload inventory images to ML before validate/publish - Show fee breakdown and net earnings in validate modal - Fallback to approximate fees (13-18%) if ML API fails
This commit is contained in:
@@ -229,6 +229,68 @@ def build_item_payload(
|
||||
return payload
|
||||
|
||||
|
||||
def _upload_images_to_meli(svc: MeliService, image_urls: list[str]) -> list[str]:
|
||||
"""Upload images to MercadoLibre and return their secure URLs.
|
||||
|
||||
If an image is already a ML URL, it's passed through unchanged.
|
||||
"""
|
||||
results = []
|
||||
for url in image_urls:
|
||||
if not url:
|
||||
continue
|
||||
# Already a ML hosted image?
|
||||
if "mercadolibre" in url or "mercadoli.com" in url:
|
||||
results.append(url)
|
||||
continue
|
||||
try:
|
||||
pic = svc.upload_image(url)
|
||||
secure_url = pic.get("secure_url") or pic.get("url") or url
|
||||
results.append(secure_url)
|
||||
except Exception as e:
|
||||
logger.warning("Failed to upload image %s to ML: %s", url, e)
|
||||
# Fallback: try using the original URL anyway
|
||||
results.append(url)
|
||||
return results
|
||||
|
||||
|
||||
def _calculate_meli_net_price(svc: MeliService, site_id: str, price: float, listing_type_id: str, category_id: str) -> dict:
|
||||
"""Calculate net amount after ML commissions.
|
||||
|
||||
Returns dict with fee_amount, net_amount, fee_pct.
|
||||
Falls back to approximate percentages if the API call fails.
|
||||
"""
|
||||
try:
|
||||
fee_info = svc.get_listing_price(site_id, price, listing_type_id, category_id)
|
||||
# Response structure varies; try common keys
|
||||
sale_fee = fee_info.get("sale_fee_amount") or fee_info.get("fee_amount") or 0
|
||||
net_amount = fee_info.get("net_amount") or fee_info.get("net_receive_amount")
|
||||
if net_amount is None:
|
||||
net_amount = price - sale_fee
|
||||
return {
|
||||
"fee_amount": float(sale_fee),
|
||||
"net_amount": float(net_amount),
|
||||
"fee_pct": round((sale_fee / price) * 100, 2) if price > 0 else 0,
|
||||
"source": "ml_api",
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning("Failed to fetch ML listing price for fee calc: %s", e)
|
||||
# Fallback approximations for MLM
|
||||
approx_fees = {
|
||||
"gold_special": 0.13,
|
||||
"gold_pro": 0.16,
|
||||
"gold_premium": 0.18,
|
||||
"free": 0.0,
|
||||
}
|
||||
fee_rate = approx_fees.get(listing_type_id, 0.13)
|
||||
fee_amount = round(price * fee_rate, 2)
|
||||
return {
|
||||
"fee_amount": fee_amount,
|
||||
"net_amount": round(price - fee_amount, 2),
|
||||
"fee_pct": round(fee_rate * 100, 2),
|
||||
"source": "approximation",
|
||||
}
|
||||
|
||||
|
||||
def check_meli_shipping_config(svc: MeliService, cfg: dict) -> dict:
|
||||
"""Check if the user's ML account has the required shipping modes configured.
|
||||
|
||||
@@ -394,6 +456,17 @@ def validate_items(
|
||||
results["invalid"].append({"inventory_id": inv_id, "error": "El producto no tiene imagen"})
|
||||
continue
|
||||
|
||||
# Upload images to ML hosting so they are publicly accessible
|
||||
try:
|
||||
ml_images = _upload_images_to_meli(svc, images)
|
||||
except Exception as e:
|
||||
results["invalid"].append({"inventory_id": inv_id, "error": f"Error subiendo imagen a ML: {e}"})
|
||||
continue
|
||||
|
||||
if not ml_images:
|
||||
results["invalid"].append({"inventory_id": inv_id, "error": "No se pudo subir la imagen a MercadoLibre"})
|
||||
continue
|
||||
|
||||
title = (custom_data.get("titles") or {}).get(str(inv_id))
|
||||
extra_attrs = (custom_data.get("attributes") or {}).get(str(inv_id))
|
||||
price = (custom_data.get("prices") or {}).get(str(inv_id), inv["price_1"])
|
||||
@@ -401,7 +474,7 @@ def validate_items(
|
||||
shipping_cost = custom_data.get("shipping_cost")
|
||||
|
||||
payload = build_item_payload(
|
||||
inv, images, meli_category_id, price, item_stock,
|
||||
inv, ml_images, meli_category_id, price, item_stock,
|
||||
shipping_mode=shipping_mode, listing_type_id=listing_type_id,
|
||||
custom_title=title, extra_attributes=extra_attrs,
|
||||
shipping_cost=shipping_cost,
|
||||
@@ -411,7 +484,18 @@ def validate_items(
|
||||
try:
|
||||
validation = svc.validate_item(payload)
|
||||
if validation.get("status") == "valid":
|
||||
results["valid"].append({"inventory_id": inv_id, "validation": validation})
|
||||
# Calculate estimated net earnings after ML fees
|
||||
site_id = cfg.get("meli_site_id", "MLM")
|
||||
fee_calc = _calculate_meli_net_price(svc, site_id, float(price), listing_type_id, meli_category_id)
|
||||
results["valid"].append({
|
||||
"inventory_id": inv_id,
|
||||
"validation": validation,
|
||||
"price": float(price),
|
||||
"fee_amount": fee_calc["fee_amount"],
|
||||
"net_amount": fee_calc["net_amount"],
|
||||
"fee_pct": fee_calc["fee_pct"],
|
||||
"fee_source": fee_calc["source"],
|
||||
})
|
||||
else:
|
||||
errors = validation.get("validation_errors", [])
|
||||
error_msgs = [f"{e.get('field', '')}: {e.get('message', '')}" for e in errors]
|
||||
@@ -504,6 +588,17 @@ def publish_items(
|
||||
results["failed"].append({"inventory_id": inv_id, "error": "El producto no tiene imagen. ML requiere imagen para publicar."})
|
||||
continue
|
||||
|
||||
# Upload images to ML hosting so they are publicly accessible
|
||||
try:
|
||||
ml_images = _upload_images_to_meli(svc, images)
|
||||
except Exception as e:
|
||||
results["failed"].append({"inventory_id": inv_id, "error": f"Error subiendo imagen a ML: {e}"})
|
||||
continue
|
||||
|
||||
if not ml_images:
|
||||
results["failed"].append({"inventory_id": inv_id, "error": "No se pudo subir la imagen a MercadoLibre"})
|
||||
continue
|
||||
|
||||
title = (custom_data.get("titles") or {}).get(str(inv_id))
|
||||
extra_attrs = (custom_data.get("attributes") or {}).get(str(inv_id))
|
||||
price = (custom_data.get("prices") or {}).get(str(inv_id), inv["price_1"])
|
||||
@@ -511,7 +606,7 @@ def publish_items(
|
||||
shipping_cost = custom_data.get("shipping_cost")
|
||||
|
||||
payload = build_item_payload(
|
||||
inv, images, meli_category_id, price, item_stock,
|
||||
inv, ml_images, meli_category_id, price, item_stock,
|
||||
shipping_mode=shipping_mode, listing_type_id=listing_type_id,
|
||||
custom_title=title, extra_attributes=extra_attrs,
|
||||
shipping_cost=shipping_cost,
|
||||
|
||||
Reference in New Issue
Block a user