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:
@@ -143,6 +143,66 @@ class MeliService:
|
||||
)
|
||||
return resp.json()
|
||||
|
||||
# ─── Images ──────────────────────────────────────────────────────────
|
||||
|
||||
def upload_image(self, image_path_or_url: str) -> dict:
|
||||
"""Upload an image to MercadoLibre's image hosting.
|
||||
|
||||
Accepts either a local file path or a URL.
|
||||
Returns the ML picture dict with 'id' and 'secure_url' / 'url' keys.
|
||||
"""
|
||||
import os
|
||||
import requests as raw_requests
|
||||
|
||||
# If it's a URL, download it first
|
||||
if image_path_or_url.startswith("http://") or image_path_or_url.startswith("https://"):
|
||||
img_resp = raw_requests.get(image_path_or_url, timeout=30)
|
||||
if not img_resp.ok:
|
||||
raise MeliError(f"Failed to download image from {image_path_or_url}: {img_resp.status_code}")
|
||||
file_bytes = img_resp.content
|
||||
content_type = img_resp.headers.get("Content-Type", "image/jpeg")
|
||||
filename = "image.jpg"
|
||||
else:
|
||||
if not os.path.exists(image_path_or_url):
|
||||
raise MeliError(f"Image file not found: {image_path_or_url}")
|
||||
with open(image_path_or_url, "rb") as f:
|
||||
file_bytes = f.read()
|
||||
content_type = "image/jpeg"
|
||||
filename = os.path.basename(image_path_or_url)
|
||||
|
||||
upload_url = f"{BASE_URL}/pictures"
|
||||
files = {"file": (filename, file_bytes, content_type)}
|
||||
req_headers = {"Authorization": f"Bearer {self.access_token}"}
|
||||
resp = raw_requests.post(upload_url, files=files, headers=req_headers, timeout=60)
|
||||
|
||||
if resp.status_code == 401 and self.refresh_token:
|
||||
self._refresh_token()
|
||||
req_headers["Authorization"] = f"Bearer {self.access_token}"
|
||||
resp = raw_requests.post(upload_url, files=files, headers=req_headers, timeout=60)
|
||||
|
||||
if resp.status_code == 401:
|
||||
raise MeliAuthError("Unauthorized. Token may be expired.", status_code=401, response_body=resp.text)
|
||||
if not resp.ok:
|
||||
raise MeliError(f"Image upload failed: {resp.status_code} {resp.text}", status_code=resp.status_code, response_body=resp.text)
|
||||
|
||||
return resp.json()
|
||||
|
||||
def get_listing_price(self, site_id: str, price: float, listing_type_id: str, category_id: str) -> dict:
|
||||
"""Get the exact fee / net amount for a given price, listing type and category.
|
||||
|
||||
ML endpoint: GET /sites/{site_id}/listing_prices
|
||||
Returns dict with sale_fee_amount, net_amount, etc.
|
||||
"""
|
||||
return self._request(
|
||||
"GET",
|
||||
f"/sites/{site_id}/listing_prices",
|
||||
params={
|
||||
"price": str(price),
|
||||
"listing_type_id": listing_type_id,
|
||||
"category_id": category_id,
|
||||
},
|
||||
)
|
||||
|
||||
# ─── Items (listings) ────────────────────────────────────────────────
|
||||
|
||||
def validate_item(self, payload: dict) -> dict:
|
||||
|
||||
Reference in New Issue
Block a user