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