← back to Shopify Sample Shipping

proto-autoapply.mjs

123 lines

#!/usr/bin/env node
// TK-11333 — PROTOTYPE HARNESS (go/no-go for flipping). Answers the one unproven mechanic
// (DTD dissent hole #1): does the /discount/CODE?redirect=/cart permalink actually make FREE
// SHIPPING persist to CHECKOUT for a logged-in ELIGIBLE customer — or does it degrade to a
// page-reload / lost-on-reload / still-charged experience?
//
// It drives the REAL Chrome over CDP (openclaw, 127.0.0.1:18800 — same seam as admin-browser.mjs)
// so it uses whatever customer session is logged in THERE. Steve: in that Chrome, log in as a
// throwaway ELIGIBLE (in-segment) customer and have a saved US address before running with
// --checkout. The harness only manipulates a customer CART/checkout session (no admin writes).
//
// PREREQ: the test code must already exist. Either create TRADESHIP first (create-freeship-codes)
// or have Steve make a throwaway free-ship code in Admin — note it as a runbook step.
//
//   node proto-autoapply.mjs --code TRADESHIP --variant 44090076954675 --checkout
//     --domain designerwallcoverings.com   (default domain shown)
//
// VERDICT (printed + verification/proto-autoapply-report.json):
//   PASS    — checkout shows $0 shipping for the eligible customer AND it survives a reload
//   DEGRADE — permalink reloaded but code didn't stick / checkout still charges / lost on reload
//   UNKNOWN — couldn't complete (CDP down / not logged in / no address / no checkout reached)
import fs from 'node:fs';

const arg = k => { const i = process.argv.indexOf('--' + k); return i >= 0 ? process.argv[i + 1] : undefined; };
const CODE = arg('code');
const DOMAIN = arg('domain') || 'designerwallcoverings.com';
const VARIANT = arg('variant');            // a Sample variant numeric id to add to cart
const DO_CHECKOUT = process.argv.includes('--checkout');
const CDP = arg('cdp') || 'http://127.0.0.1:18800';
if (!CODE) { console.error('need --code CODE'); process.exit(1); }

const OUT = new URL('./verification/proto-autoapply-report.json', import.meta.url);
const rep = { at: new Date().toISOString(), code: CODE, domain: DOMAIN, variant: VARIANT || null, steps: {}, verdict: 'UNKNOWN', notes: [] };
const base = `https://${DOMAIN}`;

let chromium;
try { ({ chromium } = await import('../Designer-Wallcoverings/node_modules/playwright/index.mjs')); }
catch (e) { rep.notes.push('playwright import failed: ' + e.message); finish(); }

let b;
try { b = await chromium.connectOverCDP(CDP); }
catch (e) { rep.notes.push('CDP connect failed (' + CDP + '): ' + e.message + ' — start openclaw real Chrome, then retry.'); finish(); }

try {
  const ctx = b.contexts()[0] || await b.newContext();
  const page = await ctx.newPage();

  // 0) login state
  await page.goto(`${base}/account`, { waitUntil: 'domcontentloaded', timeout: 45000 });
  const onLogin = /\/account\/login/.test(page.url());
  rep.steps.loggedIn = !onLogin;
  if (onLogin) rep.notes.push('NOT logged in — log in as an eligible (in-segment) customer in the CDP Chrome, then re-run.');

  // 1) add a sample to cart (if a variant was provided)
  if (VARIANT) {
    const added = await page.evaluate(async (v) => {
      const r = await fetch('/cart/add.js', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ items: [{ id: Number(v), quantity: 1 }] }) });
      return { ok: r.ok, status: r.status };
    }, VARIANT);
    rep.steps.addToCart = added;
  }
  const cartBefore = await page.evaluate(async () => (await (await fetch('/cart.js')).json()));
  rep.steps.cartBefore = { item_count: cartBefore.item_count, total_price: cartBefore.total_price, total_discount: cartBefore.total_discount };

  // 2) hit the discount permalink (this NAVIGATES = the "reload" the dissent worries about)
  const t0 = Date.now();
  await page.goto(`${base}/discount/${encodeURIComponent(CODE)}?redirect=/cart`, { waitUntil: 'domcontentloaded', timeout: 45000 });
  rep.steps.permalink = { landedUrl: page.url(), navMs: Date.now() - t0, causedReload: true };
  const cartAfter = await page.evaluate(async () => (await (await fetch('/cart.js')).json()));
  rep.steps.cartAfter = { item_count: cartAfter.item_count, total_price: cartAfter.total_price, total_discount: cartAfter.total_discount, discount_apps: cartAfter.cart_level_discount_applications?.length ?? null };
  const codeStuckInCart = (cartAfter.total_discount > 0) || (cartAfter.cart_level_discount_applications?.length > 0);
  rep.steps.codeStuckInCart = codeStuckInCart;
  // NOTE: cart.js does NOT reflect a SHIPPING discount (shipping is priced at checkout) — so
  // a free-shipping code correctly shows total_discount:0 here. Presence of the code cookie is
  // proven by reaching checkout below. We record cart state for diagnostics either way.

  // 3) persistence across a reload of /cart
  await page.reload({ waitUntil: 'domcontentloaded' });
  const cartReload = await page.evaluate(async () => (await (await fetch('/cart.js')).json()));
  rep.steps.cartAfterReload = { total_price: cartReload.total_price, total_discount: cartReload.total_discount };

  // 4) checkout: the ONLY place a free-shipping code proves itself
  if (DO_CHECKOUT) {
    try {
      await page.goto(`${base}/checkout`, { waitUntil: 'domcontentloaded', timeout: 60000 });
      await page.waitForTimeout(3500);
      const url = page.url();
      const body = (await page.locator('body').innerText().catch(() => '')).slice(0, 20000);
      // capture any shipping line + free markers
      const shipLine = (body.match(/shipping[^\n]{0,60}/i) || [])[0] || '';
      const freeMarker = /free/i.test(shipLine) || /\$?0\.00\s*(shipping|delivery)/i.test(body);
      const shot = new URL('./verification/proto-checkout.png', import.meta.url).pathname;
      await page.screenshot({ path: shot, fullPage: false }).catch(() => {});
      rep.steps.checkout = { url, shippingLine: shipLine.replace(/\s+/g, ' ').trim(), freeMarker, screenshot: shot, onCheckoutDomain: /\/checkout|checkouts\//.test(url) };
      // reload persistence at checkout
      await page.reload({ waitUntil: 'domcontentloaded' }); await page.waitForTimeout(2500);
      const body2 = (await page.locator('body').innerText().catch(() => '')).slice(0, 20000);
      rep.steps.checkout.freeAfterReload = /free/i.test((body2.match(/shipping[^\n]{0,60}/i) || [''])[0]) || /\$?0\.00\s*(shipping|delivery)/i.test(body2);
    } catch (e) { rep.notes.push('checkout step error: ' + e.message); }
  }

  // ---- VERDICT ----
  if (!rep.steps.loggedIn) { rep.verdict = 'UNKNOWN'; rep.notes.push('verdict UNKNOWN — not logged in; the eligible-customer path is the whole test.'); }
  else if (DO_CHECKOUT && rep.steps.checkout?.onCheckoutDomain) {
    if (rep.steps.checkout.freeMarker && rep.steps.checkout.freeAfterReload) { rep.verdict = 'PASS'; rep.notes.push('Free shipping showed at checkout AND survived a reload — auto-apply is viable.'); }
    else if (rep.steps.checkout.freeMarker) { rep.verdict = 'DEGRADE'; rep.notes.push('Free shipping showed at checkout but did NOT survive a reload — session-only, flaky.'); }
    else { rep.verdict = 'DEGRADE'; rep.notes.push('Reached checkout but shipping was NOT free — code did not carry to checkout.'); }
  } else { rep.verdict = 'UNKNOWN'; rep.notes.push('Ran without --checkout (or checkout not reached). Re-run with --checkout + a logged-in eligible customer + saved address for a real go/no-go.'); }

  await b.close().catch(() => {});
} catch (e) { rep.notes.push('harness error: ' + e.message); try { await b.close(); } catch {} }

finish();
function finish() {
  fs.writeFileSync(OUT, JSON.stringify(rep, null, 2) + '\n');
  console.log('=== proto-autoapply — VERDICT:', rep.verdict, '===');
  for (const [k, v] of Object.entries(rep.steps)) console.log('  ' + k + ':', JSON.stringify(v));
  if (rep.notes.length) { console.log('notes:'); rep.notes.forEach(n => console.log('  - ' + n)); }
  console.log('\nfull report -> verification/proto-autoapply-report.json');
  console.log('\nGO/NO-GO: PASS => safe to create codes + flip band. DEGRADE => do NOT remove the band; keep a fallback band or rethink auto-apply. UNKNOWN => complete the manual prereqs and re-run.');
  process.exit(0);
}