← back to Kravet Sheet Sync 2026 04 20

fix_audit_issues.py

203 lines

#!/usr/bin/env python3
"""
Fix issues from audit_last_30:
  A) Add 5 active products missing from New Arrivals collection
  B) Fix 3 DRAFT products with FAILED images — upload from FTP mirror via
     stagedUploadsCreate, then attach and re-activate
"""
import json, os, subprocess, urllib.request, urllib.parse

TOKEN=os.environ.get('SHOPIFY_ADMIN_TOKEN','')
SHOP='designer-laboratory-sandbox.myshopify.com'
URL=f'https://{SHOP}/admin/api/2024-10/graphql.json'
NEW_ARRIVALS='gid://shopify/Collection/167327760435'
SSH=['ssh','root@45.61.58.125']

MISSING_COLLECTION_ACTIVE = [
    "gid://shopify/Product/7821710032947",  # DWKK-142909 Tall Trees G P & J Baker
    "gid://shopify/Product/7821710065715",  # DWKK-143582 Songbird Dawn
    "gid://shopify/Product/7821710131251",  # DWKK-144813 Mandalay Sisal Willow
    "gid://shopify/Product/7821740605491",  # DWKK-155345 Fallingwater Charcoal Threads
]

IMAGE_FAILED = [
    # DWKK, product_gid, mfr_sku, remote FTP path, local Kamatera mirror path
    {"dw_sku":"DWKK-155119","pid":"gid://shopify/Product/7821740539955",
     "mfr":"FG125.S110.0","title":"Grand Mulberry Woodland Summer | Mulberry",
     "ftp_path":"/KF/HIRES/FG125_S110.JPG"},
    {"dw_sku":"DWKK-155304","pid":"gid://shopify/Product/7821740572723",
     "mfr":"NZAW112026.SCN.0","title":"Parlour Palm | Scion",
     "ftp_path":"/KF/HIRES/NZAW112026_SCN.JPG"},
    {"dw_sku":"DWKK-156194","pid":"gid://shopify/Product/7821740638259",
     "mfr":"WHF3846.WT.0","title":"Compass Clay | Winfield Thybony",
     "ftp_path":"/KF/HIRES/WHF3846_WT.JPG"},
]


def gql(q, v=None):
    body=json.dumps({"query":q,"variables":v or {}}).encode()
    req=urllib.request.Request(URL,data=body,method="POST",
        headers={"Content-Type":"application/json","X-Shopify-Access-Token":TOKEN})
    d=json.loads(urllib.request.urlopen(req,timeout=30).read())
    if "errors" in d: raise RuntimeError(d["errors"])
    return d["data"]


# ============================================================
# A) Add missing products to New Arrivals collection
# ============================================================
print("=== A) Adding missing products to New Arrivals ===")
COL_ADD = """mutation($id:ID!,$pids:[ID!]!){collectionAddProducts(id:$id,productIds:$pids){userErrors{field message}}}"""
d = gql(COL_ADD, {"id": NEW_ARRIVALS, "pids": MISSING_COLLECTION_ACTIVE})
errs = d["collectionAddProducts"]["userErrors"]
print(f"  added {len(MISSING_COLLECTION_ACTIVE)} products to New Arrivals" + (f" (errors: {errs})" if errs else ""))


# ============================================================
# B) Fix 3 DRAFT products with failed images
# ============================================================
print("\n=== B) Fixing DRAFT products with failed Brandfolder fetches ===")

# Check what's on the FTP mirror for each — verify file exists
for item in IMAGE_FAILED:
    r = subprocess.run(SSH + [f"ls -la /root/kravet-ftp-mirror{item['ftp_path']} 2>&1 || echo MISSING"],
                       capture_output=True, text=True)
    item["ftp_exists"] = "MISSING" not in r.stdout
    print(f"  {item['dw_sku']} ftp {item['ftp_path']}: {'✓' if item['ftp_exists'] else '✗ MISSING'}")

# For each missing file, try finding actual filename
# Look for alt brandfolder URL in kravet_sku_images
import io, csv
for item in IMAGE_FAILED:
    if item["ftp_exists"]:
        continue
    esc_sku = item["mfr"].replace("'", "''")
    r = subprocess.run(SSH + [
        f'PGPASSWORD=DW2024! psql -h 127.0.0.1 -U dw_admin -d dw_unified --csv -t -c '
        f'"SELECT url FROM kravet_sku_images WHERE mfr_sku_raw = \'{esc_sku}\' ORDER BY kind, url LIMIT 3"'],
        capture_output=True, text=True)
    urls = [r[0] for r in csv.reader(io.StringIO(r.stdout)) if r]
    print(f"  {item['dw_sku']} fallback URLs from catalog ({len(urls)}):")
    for u in urls: print(f"    {u[:130]}")
    item["fallback_urls"] = urls


# Upload an image for each missing product via stagedUploadsCreate
STAGED_UP = """
mutation($input:[StagedUploadInput!]!){
  stagedUploadsCreate(input:$input){
    stagedTargets{ url resourceUrl parameters{ name value } }
    userErrors{ field message }
  }
}"""
MEDIA_CREATE = """
mutation($pid:ID!,$m:[CreateMediaInput!]!){
  productCreateMedia(productId:$pid,media:$m){
    media{ ... on MediaImage{ id status } }
    mediaUserErrors{ field message }
  }
}"""
MEDIA_DEL = """
mutation($pid:ID!,$ids:[ID!]!){
  productDeleteMedia(productId:$pid,mediaIds:$ids){ deletedMediaIds mediaUserErrors{ field message } }
}"""
PROD_UPD = """
mutation($p:ProductUpdateInput!){productUpdate(product:$p){product{id status} userErrors{field message}}}"""


def fetch_from_kamatera(remote_path, local_path):
    """SCP a single file from Kamatera mirror to local."""
    r = subprocess.run(["scp", f"root@45.61.58.125:{remote_path}", local_path],
                       capture_output=True, text=True)
    return r.returncode == 0 and os.path.exists(local_path) and os.path.getsize(local_path) > 0


def staged_upload_shopify(local_path, mime="image/jpeg"):
    fsize = os.path.getsize(local_path)
    fname = os.path.basename(local_path)
    d = gql(STAGED_UP, {"input": [{
        "filename": fname, "mimeType": mime, "resource": "IMAGE",
        "httpMethod": "POST", "fileSize": str(fsize),
    }]})
    errs = d["stagedUploadsCreate"]["userErrors"]
    if errs: raise RuntimeError(f"staged create: {errs}")
    target = d["stagedUploadsCreate"]["stagedTargets"][0]

    # Build multipart manually
    boundary = "----dw-form-" + os.urandom(8).hex()
    body_parts = []
    for p in target["parameters"]:
        body_parts.append(f'--{boundary}\r\nContent-Disposition: form-data; name="{p["name"]}"\r\n\r\n{p["value"]}\r\n')
    # file part
    with open(local_path, "rb") as f: file_bytes = f.read()
    body_parts.append(f'--{boundary}\r\nContent-Disposition: form-data; name="file"; filename="{fname}"\r\nContent-Type: {mime}\r\n\r\n')
    body = "".join(body_parts).encode() + file_bytes + f"\r\n--{boundary}--\r\n".encode()

    req = urllib.request.Request(target["url"], data=body, method="POST",
        headers={"Content-Type": f"multipart/form-data; boundary={boundary}"})
    urllib.request.urlopen(req, timeout=60).read()
    return target["resourceUrl"]


import os
TMP_DIR = "/tmp/kravet_fallback_images"
os.makedirs(TMP_DIR, exist_ok=True)

for item in IMAGE_FAILED:
    dw_sku, pid = item["dw_sku"], item["pid"]
    print(f"\n  [{dw_sku}] {item['title']}")

    # 1) Delete any old failed media
    cur = gql("query($id:ID!){product(id:$id){media(first:10){nodes{id}}}}", {"id": pid})
    media_ids = [m["id"] for m in cur["product"]["media"]["nodes"]]
    if media_ids:
        gql(MEDIA_DEL, {"pid": pid, "ids": media_ids})
        print(f"    deleted {len(media_ids)} failed media")

    # 2) Get image source
    local = None
    if item.get("ftp_exists"):
        local = f"{TMP_DIR}/{dw_sku}.jpg"
        if fetch_from_kamatera(f"/root/kravet-ftp-mirror{item['ftp_path']}", local):
            print(f"    pulled from FTP mirror: {os.path.getsize(local):,} bytes")
        else:
            local = None

    if not local:
        # Try catalog fallback URLs
        for u in item.get("fallback_urls", []):
            try:
                req = urllib.request.Request(u.replace(" ", "%20"),
                    headers={"User-Agent":"Mozilla/5.0"})
                b = urllib.request.urlopen(req, timeout=30).read()
                if len(b) > 20000:  # real image, not placeholder
                    local = f"{TMP_DIR}/{dw_sku}.jpg"
                    open(local, "wb").write(b)
                    print(f"    pulled from URL: {len(b):,} bytes")
                    break
            except Exception as e:
                print(f"    URL fail: {e}")
    if not local:
        print(f"    SKIP: no image source")
        continue

    # 3) Staged upload + attach
    try:
        resource_url = staged_upload_shopify(local)
        print(f"    staged → {resource_url[:80]}")
        r = gql(MEDIA_CREATE, {"pid": pid, "m": [{
            "originalSource": resource_url,
            "mediaContentType": "IMAGE",
            "alt": item["title"],
        }]})
        errs = r["productCreateMedia"]["mediaUserErrors"]
        if errs:
            print(f"    attach errors: {errs}")
        else:
            m = r["productCreateMedia"]["media"][0]
            print(f"    attached: {m['id']} status={m['status']}")
    except Exception as e:
        print(f"    stage/attach fail: {e}")

print("\ndone.")