← 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); });