← back to Dw Signup Fulfillment

theme-proposals/loggedin-trade-entry/deploy.py

157 lines

#!/usr/bin/env python3
"""
TK-10021 — Logged-in designers can now reach Apply-for-Trade (auto-render trade-only view).

Live theme: 144396058675 (main) on designer-laboratory-sandbox.myshopify.com
Token: SHOPIFY_THEME_TOKEN (ends 2954) from ~/Projects/secrets-manager/.env

Three anchored edits:
  1. layout/theme.liquid:1360        gate  {% unless customer %} -> {% unless customer.tags contains 'trade' %}
  2. snippets/dw-signin-modal.liquid  wrap sign-in/retail/toggle/google in {% unless customer %};
                                       add logged-in trade header; default-show + prefill the trade form.
  3. templates/customers/account.liquid  add an "Apply for Trade Pricing" trigger for non-trade customers.

DRY-RUN by default: pulls each live asset, applies edits in-memory, prints a unified diff, writes a
timestamped local backup. Nothing is pushed. Pass --apply to PUT the patched assets back (Steve-gated).

Every anchor is asserted to appear EXACTLY ONCE before patching; any drift aborts the whole run so a
theme change under our feet can never produce a half-applied edit.
"""
import os, sys, json, difflib, urllib.request, urllib.parse, datetime, pathlib

STORE = "designer-laboratory-sandbox.myshopify.com"
THEME = "144396058675"
API   = "2024-10"
HERE  = pathlib.Path(__file__).resolve().parent
BKP   = HERE / "backups"
APPLY = "--apply" in sys.argv

def token():
    env = pathlib.Path.home() / "Projects/secrets-manager/.env"
    for line in env.read_text().splitlines():
        if line.startswith("SHOPIFY_THEME_TOKEN="):
            return line.split("=", 1)[1].strip().strip('"').strip("'")
    sys.exit("SHOPIFY_THEME_TOKEN not found")

TOK = token()

def _req(method, key, value=None):
    url = f"https://{STORE}/admin/api/{API}/themes/{THEME}/assets.json"
    if method == "GET":
        url += "?asset[key]=" + urllib.parse.quote(key)
        data = None
    else:
        data = json.dumps({"asset": {"key": key, "value": value}}).encode()
    r = urllib.request.Request(url, data=data, method=method,
                              headers={"X-Shopify-Access-Token": TOK,
                                       "Content-Type": "application/json"})
    with urllib.request.urlopen(r) as resp:
        return json.load(resp)

def get_asset(key):
    return _req("GET", key)["asset"]["value"]

def put_asset(key, value):
    _req("PUT", key, value)          # Shopify validates Liquid on PUT; 200 == valid

def once(hay, needle, key):
    n = hay.count(needle)
    if n != 1:
        sys.exit(f"ABORT [{key}]: anchor appears {n}x (expected 1). Live theme drifted:\n  {needle[:80]!r}")

# ---- edit 1: theme.liquid gate --------------------------------------------
def patch_theme(v):
    a = "{% unless customer %}{% render 'dw-signin-modal' %}{% endunless %}"
    b = "{% unless customer.tags contains 'trade' %}{% render 'dw-signin-modal' %}{% endunless %}"
    once(v, a, "theme.liquid"); return v.replace(a, b)

# ---- edit 2: dw-signin-modal.liquid ---------------------------------------
def patch_modal(v):
    # (2a) open the logged-out-only run right before the returning-sign-in block
    a1 = '    <div class="dwsm-return">'
    once(v, a1, "modal/return-open")
    v = v.replace(a1, '    {%- unless customer -%}\n' + a1, 1)

    # (2b) close that run right after the Google block, before the trade form comment,
    #      then add the logged-in trade header.
    a2 = "    {%- comment -%} Moderated trade application"
    once(v, a2, "modal/trade-comment")
    header = (
        '    {%- endunless -%}\n'
        '    {%- if customer -%}\n'
        '    <div class="dwsm-return">\n'
        '      <h2>Apply for Trade Pricing</h2>\n'
        '      <p class="dwsm-return-cap">You\'re signed in — submit your business details and our team will review your trade account.</p>\n'
        '    </div>\n'
        '    {%- endif -%}\n'
    )
    v = v.replace(a2, header + a2, 1)

    # (2c) trade form: default-show for logged-in, prefill business name + email
    a3 = '<form class="dwsm-trade" data-dwsm-trade style="display:none;" novalidate>'
    once(v, a3, "modal/form")
    v = v.replace(a3, '<form class="dwsm-trade" data-dwsm-trade style="display:{% if customer %}block{% else %}none{% endif %};" novalidate>')

    a4 = '<input type="text" name="business_name" autocomplete="organization" required>'
    once(v, a4, "modal/bizname")
    v = v.replace(a4, '<input type="text" name="business_name" autocomplete="organization" value="{{ customer.default_address.company }}" required>')

    a5 = '<input type="email" name="email" autocomplete="email" required>'
    once(v, a5, "modal/email")
    v = v.replace(a5, '<input type="email" name="email" autocomplete="email" value="{{ customer.email }}" required>')

    # (2d) wrap the retail sign-in/create block as logged-out-only
    a6 = ('    <div data-dwsm-retail>\n'
          '      <input type="button" class="dwsm-submit" data-dwsm-login="{{ routes.account_login_url }}" value="Sign In / Create Account">\n\n'
          '      <div class="dwsm-foot">\n'
          '        New to Designer Wallcoverings? <a href="{{ routes.account_register_url }}">Create account</a>\n'
          '      </div>\n'
          '    </div>')
    once(v, a6, "modal/retail-block")
    v = v.replace(a6, '    {%- unless customer -%}\n' + a6 + '\n    {%- endunless -%}')
    return v

# ---- edit 3: account.liquid trigger ---------------------------------------
def patch_account(v):
    a = "  {% render 'breadcrumbs' %}"
    once(v, a, "account/breadcrumbs")
    cta = (a + '\n\n'
           "  {%- unless customer.tags contains 'trade' -%}\n"
           '    <p class="dw-trade-cta" style="margin:16px 0;">\n'
           '      <a href="#dw-signin" data-dw-signin style="color:#b08212;font-weight:600;text-decoration:none;">Apply for Trade Pricing →</a>\n'
           '    </p>\n'
           '  {%- endunless -%}')
    return v.replace(a, cta, 1)

EDITS = [
    ("layout/theme.liquid",              patch_theme),
    ("snippets/dw-signin-modal.liquid",  patch_modal),
    ("templates/customers/account.liquid", patch_account),
]

def main():
    stamp = datetime.datetime.now().strftime("%Y%m%dT%H%M%S")
    BKP.mkdir(parents=True, exist_ok=True)
    changed = []
    for key, fn in EDITS:
        live = get_asset(key)
        (BKP / (key.replace("/", "__") + f".{stamp}.bak")).write_text(live)
        new = fn(live)
        if new == live:
            print(f"[= ] {key}: no change (already applied?)"); continue
        diff = difflib.unified_diff(live.splitlines(), new.splitlines(),
                                    fromfile=f"LIVE/{key}", tofile=f"NEW/{key}", lineterm="")
        print("\n".join(diff)); print()
        changed.append((key, new))
    if not changed:
        print("Nothing to change."); return
    if not APPLY:
        print(f"\nDRY-RUN. Backups in {BKP}. Re-run with --apply to PUT {len(changed)} asset(s) to the LIVE theme.")
        return
    for key, new in changed:
        put_asset(key, new); print(f"[PUT] {key}  (200 = Liquid valid)")
    print("\nDeployed. Verify: open the store signed in as a NON-trade customer -> account page shows 'Apply for Trade Pricing'.")

if __name__ == "__main__":
    main()