← back to CelebritySignatures

scripts/printify-cost-check.mjs

130 lines

#!/usr/bin/env node
// ONE-OFF, throwaway cost + placement-verification helper for TK-10286.
//
// For each candidate garment: uploads a REAL signature PNG (Samuel Adams —
// public-domain-historical, already wear-eligible), creates ONE minimal
// single-variant, unpublished (visible:false) draft product with the print
// positioned at the SAME x/y/scale wear-templates.json already carries, then
// fetches the product back to read (a) real Printify cost and (b) any
// auto-generated mockup image URLs so left-chest placement can be eyeballed
// visually — then IMMEDIATELY deletes the product and verifies the delete
// with a follow-up GET (expects 404). Logs created→deleted id pairs so
// nothing is left dangling, and deletes the uploaded image too.
//
// Creates NOTHING else — no order, no charge, nothing published.
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { join } from 'node:path';
import { createInterface } from 'node:readline/promises';
import { stdin, stdout } from 'node:process';

const ROOT = fileURLToPath(new URL('..', import.meta.url));
const API = 'https://api.printify.com/v1';
const SHOP_ID = 496929; // Designer Wallcoverings and Fabrics — confirmed by Steve 2026-09-08
const SIG_IMAGE_URL = 'https://upload.wikimedia.org/wikipedia/commons/0/0a/Samuel_Adams_signature.png';

function envVal(name) {
  if (process.env[name]) return process.env[name];
  for (const p of [join(ROOT, '.env'), join(ROOT, '..', 'secrets-manager', '.env')]) {
    try { const m = readFileSync(p, 'utf8').match(new RegExp('^' + name + '=(.+)$', 'm')); if (m) return m[1].trim(); } catch {}
  }
  return null;
}
const token = envVal('PRINTIFY_API_TOKEN');
if (!token) { console.error('PRINTIFY_API_TOKEN not found'); process.exit(1); }

async function req(method, path, body) {
  const r = await fetch(API + path, {
    method, headers: { Authorization: `Bearer ${token}`, 'User-Agent': 'CelebritySignatures/1.0', 'Content-Type': 'application/json' },
    body: body ? JSON.stringify(body) : undefined,
  });
  const j = await r.json().catch(() => ({}));
  return { status: r.status, ok: r.ok, body: j };
}

// CLI: node scripts/printify-cost-check.mjs [garment ...] [--yes]
//   garments: bucket-hat crew-socks classic-tee polo   (default: bucket-hat crew-socks)
//   --yes : non-interactive — delete the throwaway product immediately, no Enter prompt.
const NON_INTERACTIVE = process.argv.includes('--yes');
const wanted = process.argv.slice(2).filter(a => a !== '--yes');
// Each candidate carries its OWN placeholder + position. Cost is placement-INDEPENDENT,
// but product creation requires a valid placeholder for that blueprint.
const ALL_CANDIDATES = [
  { garment: 'bucket-hat', blueprintId: 1698, printProviderId: 99, variantId: 116655, label: 'Bucket Hat bone/One size', pos: { placeholder: 'front', x: 0.5, y: 0.5, scale: 0.7 } },
  { garment: 'dad-cap', blueprintId: 1447, printProviderId: 99, variantId: 105381, label: 'Dad Cap white/One size', pos: { placeholder: 'front_dtf', x: 0.5, y: 0.5, scale: 0.7 } },
  { garment: 'trucker-cap', blueprintId: 1692, printProviderId: 99, variantId: 116762, label: 'Trucker Cap white/One size', pos: { placeholder: 'front', x: 0.5, y: 0.5, scale: 0.7 } },
  { garment: 'crew-socks', blueprintId: 2941, printProviderId: 29, variantId: 155679, label: 'Crew Socks white/One size', pos: { placeholder: 'front', x: 0.5, y: 0.5, scale: 0.7 } },
  { garment: 'classic-tee', blueprintId: 12, printProviderId: 99, variantId: 18541, label: 'Bella+Canvas 3001 White/M', pos: { placeholder: 'front', x: 0.32, y: 0.3, scale: 0.25 } },
  { garment: 'polo', blueprintId: 1402, printProviderId: 99, variantId: 104648, label: 'JERZEES 443M Piqué Polo White/M', pos: { placeholder: 'front', x: 0.32, y: 0.3, scale: 0.25 } },
];
const CANDIDATES = wanted.length
  ? ALL_CANDIDATES.filter(c => wanted.includes(c.garment))
  : ALL_CANDIDATES.filter(c => ['bucket-hat', 'crew-socks'].includes(c.garment));
if (!CANDIDATES.length) { console.error('No matching garments. Options: bucket-hat crew-socks classic-tee polo'); process.exit(1); }

async function uploadImage() {
  console.log('Uploading test signature image (Samuel Adams — public-domain-historical)...');
  // Printify's url-fetch upload rejected this (400 / code 10300) — fetch the
  // bytes ourselves and send them as base64 'contents' instead, which is the
  // more universally-supported path (no dependency on Printify's own fetcher
  // reaching Wikimedia successfully).
  const imgResp = await fetch(SIG_IMAGE_URL);
  if (!imgResp.ok) throw new Error(`could not fetch source image: HTTP ${imgResp.status}`);
  const buf = Buffer.from(await imgResp.arrayBuffer());
  const contents = buf.toString('base64');
  const up = await req('POST', '/uploads/images.json', { file_name: 'samuel-adams-signature.png', contents });
  if (!up.ok) throw new Error(`upload failed (${up.status}): ${JSON.stringify(up.body).slice(0, 300)}`);
  console.log(`  uploaded image id=${up.body.id}`);
  return up.body.id;
}

async function main() {
  const imageId = await uploadImage();

  for (const c of CANDIDATES) {
    console.log(`\n— ${c.garment}: ${c.label} —`);
    const created = await req('POST', `/shops/${SHOP_ID}/products.json`, {
      title: `TEMP-COST-CHECK-DELETE-ME (TK-10286, ${c.garment})`,
      description: 'Throwaway — created only to read Printify cost + verify left-chest placement, deleted immediately by this script.',
      blueprint_id: c.blueprintId,
      print_provider_id: c.printProviderId,
      variants: [{ id: c.variantId, price: 3400, is_enabled: true }],
      print_areas: [{
        variant_ids: [c.variantId],
        placeholders: [{ position: c.pos.placeholder, images: [{ id: imageId, x: c.pos.x, y: c.pos.y, scale: c.pos.scale, angle: 0 }] }],
      }],
      visible: false,
    });
    if (!created.ok) { console.log(`  CREATE FAILED (${created.status}):`, JSON.stringify(created.body).slice(0, 800)); continue; }
    const productId = created.body.id;
    const variant = (created.body.variants || [])[0] || {};
    console.log(`  created product id=${productId}`);
    console.log(`  cost: $${((variant.cost || 0) / 100).toFixed(2)}  suggested retail: $${((variant.price || 0) / 100).toFixed(2)}`);

    // Fetch it back — mockup generation can lag a moment behind creation.
    await new Promise(r => setTimeout(r, 3000));
    const fetched = await req('GET', `/shops/${SHOP_ID}/products/${productId}.json`);
    const images = (fetched.ok ? fetched.body.images : created.body.images) || [];
    if (images.length) {
      console.log(`  MOCKUP IMAGES (open these to eyeball left-chest placement):`);
      for (const img of images) console.log(`    ${img.src}`);
    } else if (!NON_INTERACTIVE) {
      console.log('  no mockup images returned yet — product is still live on your account (unpublished, id=' + productId + ').');
      const rl = createInterface({ input: stdin, output: stdout });
      await rl.question('  Check https://printify.com/app/products for the mockup now if you want, then press Enter to delete it and continue... ');
      rl.close();
    }

    const del = await req('DELETE', `/shops/${SHOP_ID}/products/${productId}.json`);
    console.log(`  delete: ${del.ok ? 'OK' : 'FAILED ' + del.status}`);

    const verify = await req('GET', `/shops/${SHOP_ID}/products/${productId}.json`);
    console.log(`  verify-deleted (expect 404): got ${verify.status} — ${verify.status === 404 ? 'CONFIRMED GONE' : 'STILL PRESENT — MANUAL CLEANUP NEEDED'}`);
    console.log(`  LOG: created=${productId} deleted=${del.ok} verified_gone=${verify.status === 404}`);
  }

  const delImg = await req('DELETE', `/uploads/images/${imageId}.json`).catch(() => ({ ok: false, status: 'n/a' }));
  console.log(`\nuploaded test image cleanup: ${delImg.ok ? 'deleted' : 'left in place (id ' + imageId + ' — Printify may not support deleting an already-used upload; harmless, it is just a Samuel Adams signature PNG in your Uploads library)'}`);
}
main().catch(e => { console.error(e.message); process.exit(1); });