← back to Dw Signup Fulfillment

scripts/ship-kelly-samples.mjs

125 lines

#!/usr/bin/env node
// TK-10836 — DTD verdict B (7/7, 2026-09-10): honor the promise by SHIPPING, not by
// asking Kelly to re-run a checkout we cannot verify.
//
// Creates a $0 DRAFT ORDER for Kelly Paradis with the exact 3 swatches from her own
// abandoned cart, her own recorded address, and $0 shipping. A DRAFT (not a live order)
// on purpose: Steve reviews it in Shopify admin and clicks Complete — the final
// customer-facing act stays human, and an unwanted draft is deleted with no trace.
//
//   DRY RUN by default.  node scripts/ship-kelly-samples.mjs
//   LIVE:                node scripts/ship-kelly-samples.mjs --apply
//
// Undo: Shopify admin → Drafts → delete the draft (nothing charged, nothing shipped).

import fs from 'node:fs';
import os from 'node:os';
import config from '../lib/config.js';

const APPLY = process.argv.includes('--apply');
const SHOP = config.SHOP_DOMAIN;

// Resolving a variant by SKU needs read_products + draft-order write, which the narrow
// fulfillment token (…5f96) does NOT carry. Resolve a fuller-scoped token the same way
// Designer-Wallcoverings/shopify/scripts/strip-stale-needs-image.js does.
function resolveToken() {
  for (const k of ['SHOPIFY_FULL_ACCESS_TOKEN', 'SHOPIFY_ADMIN_TOKEN']) {
    if (process.env[k]) return process.env[k];
  }
  const candidates = [
    `${os.homedir()}/Projects/Designer-Wallcoverings/shopify/.env`,
    `${os.homedir()}/Projects/secrets-manager/.env`,
  ];
  for (const p of candidates) {
    try {
      const env = fs.readFileSync(p, 'utf8');
      const m = env.match(/^SHOPIFY_FULL_ACCESS_TOKEN=(.+)$/m)
             || env.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m)
             || env.match(/^SHOPIFY_ADMIN_API_TOKEN=(.+)$/m);
      if (m) return m[1].trim().replace(/^["']|["']$/g, '');
    } catch { /* next */ }
  }
  return config.SHOPIFY_FULFILLMENT_TOKEN;
}
const TOKEN = resolveToken();

const CUSTOMER_ID = 'gid://shopify/Customer/8388564123699';
const SKUS = [
  { sku: 'DWCC-600006-Sample', title: 'Brushed Finesse Pewter' },
  { sku: 'DWCC-600045-Sample', title: 'Shimmer Polar White' },
  { sku: 'DWCC-600128-Sample', title: 'Finesse Metallic Rose' },
];
const SHIPPING = {
  firstName: 'Kelly', lastName: 'Paradis',
  address1: '2300 Sun Valley Drive', city: 'Ann Arbor',
  provinceCode: 'MI', zip: '48108', countryCode: 'US', phone: '+17342776651',
};

async function gql(query, variables) {
  const r = await fetch(`https://${SHOP}/admin/api/2024-10/graphql.json`, {
    method: 'POST',
    headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, variables }),
  });
  const j = await r.json();
  if (j.errors) throw new Error('GraphQL: ' + JSON.stringify(j.errors).slice(0, 400));
  return j.data;
}

const LOOKUP = `query($q: String!) {
  productVariants(first: 5, query: $q) { nodes { id sku displayName price } }
}`;

const CREATE = `mutation($input: DraftOrderInput!) {
  draftOrderCreate(input: $input) {
    draftOrder { id name invoiceUrl totalPriceSet { shopMoney { amount currencyCode } } }
    userErrors { field message }
  }
}`;

(async () => {
  console.log(`ship-kelly-samples — ${APPLY ? 'APPLY (creates a live DRAFT order)' : 'DRY RUN (no writes)'}`);
  console.log(`  store=${SHOP}  token=…${String(TOKEN || '').slice(-4)}\n`);

  const lineItems = [];
  for (const { sku, title } of SKUS) {
    const d = await gql(LOOKUP, { q: `sku:${sku}` });
    const hit = (d.productVariants.nodes || []).find(v => v.sku === sku);
    if (!hit) throw new Error(`variant not found for SKU ${sku} (${title}) — resolve manually before applying`);
    console.log(`  ✓ ${sku.padEnd(24)} → ${hit.id}  $${hit.price}  ${hit.displayName}`);
    lineItems.push({ variantId: hit.id, quantity: 1, appliedDiscount: {
      valueType: 'PERCENTAGE', value: 100, title: 'Promised free sample (TK-10836)',
    }});
  }

  const input = {
    customerId: CUSTOMER_ID,
    lineItems,
    shippingAddress: SHIPPING,
    shippingLine: { title: 'Free Shipping (No Tracking)', price: '0.00' },
    tags: ['tk-10836', 'promised-free-sample', 'goodwill'],
    note: 'TK-10836 — 3 free samples promised in writing 2026-09-02 and not deliverable at '
        + 'checkout (her cart quoted $24.95 shipping on $0.00 items). DTD verdict B (7/7): '
        + 'ship directly at $0 rather than send another checkout instruction. Swatches are '
        + 'the exact 3 from her own abandoned cart 34015098110003.',
  };

  if (!APPLY) {
    console.log('\nDRY RUN — would create this draft order:');
    console.log(JSON.stringify(input, null, 2));
    console.log('\nRe-run with --apply to create it. It lands as a DRAFT for review; nothing ships until Completed.');
    return;
  }

  const res = await gql(CREATE, { input });
  const { draftOrder, userErrors } = res.draftOrderCreate;
  if (userErrors && userErrors.length) {
    console.error('USER ERRORS:', JSON.stringify(userErrors, null, 2));
    process.exit(1);
  }
  console.log(`\n✅ DRAFT created: ${draftOrder.name}  ${draftOrder.id}`);
  console.log(`   total: $${draftOrder.totalPriceSet.shopMoney.amount} ${draftOrder.totalPriceSet.shopMoney.currencyCode} (expect 0.00)`);
  console.log(`   review + Complete in Shopify admin → Orders → Drafts`);
  console.log(`   undo: delete the draft (nothing charged, nothing shipped)`);
})().catch(e => { console.error('ERROR:', e.message); process.exit(1); });