← back to Dw Yolo Loop

artmura-site/romo-reprice.py

67 lines

#!/usr/bin/env python3
"""
romo-reprice.py — reprice live Romo Single Roll variants to the catalog's correct retail.
Re-fetches each product's LIVE current price (state-drift guard) before any change.
Default = DRY-RUN (no writes). --apply performs live productVariantUpdate, ledgered before->after.
Subset: --sku <dwsku> | --canary N | --all   (from romo-reprice-candidates.json)
Env: SHOPIFY_ADMIN_TOKEN, STORE.
"""
import json, os, sys, time, urllib.request, urllib.error

TOKEN=os.environ['SHOPIFY_ADMIN_TOKEN']; STORE=os.environ['STORE']
API=f"https://{STORE}/admin/api/2024-10/graphql.json"
H={"X-Shopify-Access-Token":TOKEN,"Content-Type":"application/json"}
CANDS=os.path.join(os.path.dirname(__file__),"romo-reprice-candidates.json")
LEDGER=os.path.expanduser("~/Projects/dw-yolo-loop/romo-reprice-applied.jsonl")

def gql(q,v=None):
    for a in range(4):
        try:
            r=urllib.request.Request(API,data=json.dumps({"query":q,"variables":v or {}}).encode(),headers=H,method="POST")
            with urllib.request.urlopen(r,timeout=30) as resp: return json.loads(resp.read().decode())
        except urllib.error.HTTPError as e:
            if e.code==429: time.sleep(2*(a+1)); continue
            return {"errors":str(e)}
    return {"errors":"retries exhausted"}

def live_variant(dwsku):
    q='{ productVariants(first:1, query:"sku:%s"){edges{node{id price product{id status title}}}} }'%dwsku
    d=gql(q); e=d.get("data",{}).get("productVariants",{}).get("edges") if "data" in d else None
    if not e: return None
    n=e[0]["node"]; return {"vid":n["id"],"price":n["price"],"pid":n["product"]["id"],"status":n["product"]["status"],"title":n["product"]["title"]}

# args
apply = "--apply" in sys.argv
cands=json.load(open(CANDS))
if "--sku" in sys.argv: sel=[c for c in cands if c["dw"]==sys.argv[sys.argv.index("--sku")+1]]
elif "--canary" in sys.argv: sel=cands[:int(sys.argv[sys.argv.index("--canary")+1])]
elif "--all" in sys.argv: sel=cands
else: sel=cands[:10]

print(f"=== ROMO REPRICE {'APPLY (LIVE WRITE)' if apply else 'DRY-RUN'} — {len(sel)} SKUs ===")
print(f"{'DW SKU':<13}{'live now':>10}{'artifact':>10}{'→ new':>10}{'status':>8}  note")
led=open(LEDGER,"a") if apply else None
applied=skipped=drift=0
for c in sel:
    lv=live_variant(c["dw"])
    if not lv: print(f"{c['dw']:<13}{'—':>10}  NOT FOUND on store (skip)"); skipped+=1; continue
    live_now=float(lv["price"]); new=float(c["correct_price"]); art=float(c["live_price"])
    note=[]
    if abs(live_now-art)>=0.01: note.append(f"drift(artifact ${art:.2f})"); drift+=1
    if abs(live_now-new)<0.01: note.append("already correct");
    below = c.get("cost") and live_now<float(c["cost"])
    if below: note.append("BELOW-COST")
    flag=""
    if apply and abs(live_now-new)>=0.01:
        m='mutation($id:ID!,$p:String!){productVariantUpdate(input:{id:$id,price:$p}){productVariant{price} userErrors{message}}}'
        r=gql(m,{"id":lv["vid"],"p":f"{new:.2f}"})
        ue=r.get("data",{}).get("productVariantUpdate",{}).get("userErrors") if "data" in r else r.get("errors")
        if not ue:
            led.write(json.dumps({"dw":c["dw"],"vid":lv["vid"],"before":live_now,"after":round(new,2)})+"\n"); led.flush(); applied+=1; flag="✅ written"
        else: flag=f"❌ {str(ue)[:60]}"
    print(f"{c['dw']:<13}${live_now:>9.2f}${art:>9.2f}${new:>9.2f}{lv['status']:>8}  {' '.join(note)} {flag}")
    time.sleep(0.5)
if led: led.close()
print(f"\n=== {'applied '+str(applied)+' writes' if apply else 'DRY-RUN — nothing written'} | drift:{drift} | not-found:{skipped} ===")
if apply: print(f"reversible ledger: {LEDGER}")