← back to Tk 11331 Exec

build_d3b_manifest.py

125 lines

#!/usr/bin/env python3
"""
TK-11331 Decision-3b reprice manifest builder (READ-ONLY, deterministic).

Re-runs the EXACT d3a join+guardrail over the products d3a skipped as `no-momentum-match`,
now that the D3b recovery refresh backfilled alt_sku onto Momentum staging rows that had a
blank join key. Products that NOW map -> d3b-manifest; still-unmatched -> d3b-skips.

INPUTS (all read-only):
  data/d3a-skips.jsonl               d3a skip set (scope = reason == 'no-momentum-match')
  data/enum.jsonl                    live Shopify reads (status, variants+price, uom, width)
  data/candidates.tsv                pid|sku|width
  /tmp/mirror_x.tsv                  shopify_id\tdw_sku\tmfr_sku\tpattern_name\tvendor
  /tmp/momentum_d3b.tsv              alt_sku\tmomentum_sku\tcolor_number\tpattern_number\thw_price\tpattern_name\tcolor_name
                                     (REFRESHED momentum_colorways, post D3b alt_sku backfill)

JOIN METHODS (first hit wins, identical to d3a):
  1 exact-mfr-alt  2 tail-match-alt  3 code-map-momentum_sku  4 code-map-color_number
GUARDRAIL (all must hold, else skip+reason): exactly ONE momentum row for the key;
  hw_price non-null & >0; 15.0<=hw_price<=72.0; a sellable (non -sample) variant exists.

OUTPUTS (FILE only -- NO dw_unified/Shopify writes):
  data/d3b-manifest.jsonl   one row per newly-recovered+confidently-mapped product
  data/d3b-skips.jsonl      still-unmatched / guardrail-failed products + reason
"""
import json, collections, os
HERE = os.path.dirname(os.path.abspath(__file__)); DATA = os.path.join(HERE, "data")
MIRROR = "/tmp/mirror_x.tsv"; MOMENTUM = "/tmp/momentum_d3b.tsv"
NONYARD = {None, "Full Roll", "Sold Per None", "Sold Per EA"}
MUSTQUOTE = {"XCD-69430"}; BAND_LO, BAND_HI = 15.0, 72.0

mom = []
for l in open(MOMENTUM):
    p = l.rstrip("\n").split("\t")
    if len(p) < 7: continue
    alt, msku, cnum, pnum, hw, pat, col = p
    mom.append(dict(alt=alt.strip(), msku=msku.strip(), cnum=cnum.strip(),
                    hw=float(hw) if hw else None, pat=pat, col=col))
def idx(field):
    d = collections.defaultdict(list)
    for r in mom:
        if r[field]: d[r[field]].append(r)
    return d
by_alt, by_msku, by_cnum = idx("alt"), idx("msku"), idx("cnum")

mfr_by_gid = {}
for l in open(MIRROR):
    p = l.rstrip("\n").split("\t")
    if len(p) < 5: continue
    mfr_by_gid[p[0]] = p[2].strip()

enum = {json.loads(l)["pid"]: json.loads(l) for l in open(os.path.join(DATA, "enum.jsonl"))}
tail = lambda m: m.rsplit("_", 1)[-1] if "_" in m else m
def sellable(d):
    vs = [v for v in d["variants"] if not (v["sku"] or "").lower().endswith("-sample")]
    if not vs: return None
    vs.sort(key=lambda v: v.get("position", 99)); return vs[0]

# SCOPE = d3a no-momentum-match products
scope = [json.loads(l) for l in open(os.path.join(DATA, "d3a-skips.jsonl"))
         if json.loads(l)["reason"] == "no-momentum-match"]

man = open(os.path.join(DATA, "d3b-manifest.jsonl"), "w")
skp = open(os.path.join(DATA, "d3b-skips.jsonl"), "w")
rec = open(os.path.join(DATA, "d3b-recovered-needs-variant.jsonl"), "w")  # recovered price basis, but sample-only
meth = collections.Counter(); skipc = collections.Counter(); conf = 0; recovered_novariant = 0

for s in scope:
    pid = str(s["shopify_product_id"]); sku = s.get("sku"); width = s.get("width")
    d = enum.get(pid)
    if not d:
        skipc["not-in-enum"] += 1
        skp.write(json.dumps({**s, "d3b_reason": "product not in enum snapshot"}) + "\n"); continue
    gid = f"gid://shopify/Product/{pid}"; mfr = mfr_by_gid.get(gid, s.get("mfr_sku", "") or "")
    base = (sku or "").rsplit("-yard", 1)[0] if (sku or "").endswith("-yard") else (sku or "").rsplit("-", 1)[0]
    sv = sellable(d); cur = float(sv["price"]) if sv else None; uv = s.get("uom")
    rb = dict(shopify_product_id=pid, sku=sku, dw_sku=base, mfr_sku=mfr, current_price=cur, uom=uv, width=width)

    if base in MUSTQUOTE or (sku or "") in MUSTQUOTE:
        skipc["must-quote-mdc"] += 1
        skp.write(json.dumps({**rb, "d3b_reason": "must-quote-mdc"}) + "\n"); continue

    t = tail(mfr); hit = None; m = None
    if mfr and mfr in by_alt: hit, m = by_alt[mfr], "exact-mfr-alt"
    elif t and t in by_alt: hit, m = by_alt[t], "tail-match-alt"
    elif mfr and mfr in by_msku: hit, m = by_msku[mfr], "code-map-momentum_sku"
    elif mfr and mfr in by_cnum: hit, m = by_cnum[mfr], "code-map-color_number"

    if not hit:
        skipc["still-no-momentum-match"] += 1
        skp.write(json.dumps({**rb, "d3b_reason": "still-absent-from-live-feed (discontinued / true must-quote)"}) + "\n"); continue
    if len(hit) > 1:
        skipc["ambiguous-multi-row"] += 1
        skp.write(json.dumps({**rb, "d3b_reason": f"ambiguous:{len(hit)}-momentum-rows"}) + "\n"); continue
    r = hit[0]; hw = r["hw"]
    if hw is None or hw <= 0:
        skipc["hw-null-or-zero"] += 1
        skp.write(json.dumps({**rb, "d3b_reason": f"hw_price null/zero ({hw})", "source_momentum_pattern": r["pat"]}) + "\n"); continue
    if not (BAND_LO <= hw <= BAND_HI):
        skipc["out-of-band"] += 1
        skp.write(json.dumps({**rb, "d3b_reason": f"hw_price {hw} outside ${BAND_LO:.0f}-{BAND_HI:.0f}/yd band", "source_momentum_pattern": r["pat"]}) + "\n"); continue
    if sv is None:
        # Join+price+band all PASSED -> Momentum pattern genuinely RECOVERED, price basis known.
        # But this ACTIVE X-prefix product is SAMPLE-ONLY (no sellable variant) -> cannot be repriced.
        # Correct follow-on = ADD a Sold-Per-Yard variant @ hw_price (pos1), NOT a reprice.
        skipc["recovered-but-sample-only"] += 1; recovered_novariant += 1
        payload = {**rb, "d3b_reason": "recovered-price-basis-but-sample-only: add Sold-Per-Yard variant @ hw_price (NOT a reprice)",
                   "recovered_new_price": hw, "recovered_hw_price": hw, "join_method": m,
                   "source_momentum_pattern": r["pat"], "source_momentum_color": r["col"], "source_momentum_alt_sku": r["alt"]}
        skp.write(json.dumps(payload) + "\n"); rec.write(json.dumps(payload) + "\n"); continue

    conf += 1; meth[m] += 1
    man.write(json.dumps(dict(
        shopify_product_id=pid, variant_id=sv["id"], sku=sv["sku"], current_price=cur,
        source_momentum_pattern=r["pat"], source_momentum_color=r["col"], source_momentum_alt_sku=r["alt"],
        new_price=hw, hw_price=hw, join_method=m, mfr_sku=mfr, uom=uv, width=width,
        recovered_via="d3b-alt_sku-backfill")) + "\n")

man.close(); skp.close(); rec.close()
print("scope (d3a no-momentum-match):", len(scope))
print("d3b reprice-manifest rows (recovered + has sellable variant):", conf)
print("recovered price basis but SAMPLE-ONLY (needs add-yard-variant, not reprice):", recovered_novariant)
print("join-method breakdown:", dict(meth))
print("skip breakdown:", dict(skipc), "total skipped:", sum(skipc.values()))