← back to CelebritySignatures
add gated Printify fulfillment path for wear
4794797e5836ae223e93a87ea217922ed21725ce · 2026-09-03 13:55:41 -0700 · Steve Abrams
Files touched
M data/wear-templates.jsonA scripts/fetch-printify-catalog.mjsA scripts/submit-pod-draft-printify.mjsM scripts/submit-pod-draft.mjsM server.jsA test/printify-pod.test.mjsA verification/e2e-proof-printify.json
Diff
commit 4794797e5836ae223e93a87ea217922ed21725ce
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Sep 3 13:55:41 2026 -0700
add gated Printify fulfillment path for wear
---
data/wear-templates.json | 14 ++++++
scripts/fetch-printify-catalog.mjs | 63 ++++++++++++++++++++++++
scripts/submit-pod-draft-printify.mjs | 92 +++++++++++++++++++++++++++++++++++
scripts/submit-pod-draft.mjs | 6 +--
server.js | 31 +++++++++---
test/printify-pod.test.mjs | 33 +++++++++++++
verification/e2e-proof-printify.json | 46 ++++++++++++++++++
7 files changed, 276 insertions(+), 9 deletions(-)
diff --git a/data/wear-templates.json b/data/wear-templates.json
index 84f9dd2..fe2b86c 100644
--- a/data/wear-templates.json
+++ b/data/wear-templates.json
@@ -64,6 +64,13 @@
"placementType": "front",
"variants": {}
},
+ "printify": {
+ "_note": "Printify dynamic-order mapping (fill after PRINTIFY_API_TOKEN lands — run scripts/fetch-printify-catalog.mjs). blueprintId + printProviderId select the blank garment; variants maps our color/size key to its variant_id. The paid order's signature image is supplied per order with left-chest positioning. Empty mappings fail loud and keep checkout disabled.",
+ "blueprintId": null,
+ "printProviderId": null,
+ "printPosition": { "placeholder": "front", "scale": 0.25, "x": 0.32, "y": 0.3 },
+ "variants": {}
+ },
"sizeUpchargeUsd": {
"2XL": 4
}
@@ -135,6 +142,13 @@
"placementType": "front",
"variants": {}
},
+ "printify": {
+ "_note": "Printify dynamic-order mapping (fill after PRINTIFY_API_TOKEN lands — run scripts/fetch-printify-catalog.mjs). blueprintId + printProviderId select the blank garment; variants maps our color/size key to its variant_id. The paid order's signature image is supplied per order with left-chest positioning. Empty mappings fail loud and keep checkout disabled.",
+ "blueprintId": null,
+ "printProviderId": null,
+ "printPosition": { "placeholder": "front", "scale": 0.25, "x": 0.32, "y": 0.3 },
+ "variants": {}
+ },
"sizeUpchargeUsd": {
"2XL": 4
}
diff --git a/scripts/fetch-printify-catalog.mjs b/scripts/fetch-printify-catalog.mjs
new file mode 100644
index 0000000..a2fcca8
--- /dev/null
+++ b/scripts/fetch-printify-catalog.mjs
@@ -0,0 +1,63 @@
+#!/usr/bin/env node
+// READ-ONLY Printify account/catalog helper for TK-10286.
+// Lists the account's shops, blank-product blueprints, print providers, and
+// variants. It never creates products, orders, or production jobs.
+import { fileURLToPath } from 'node:url';
+import { join } from 'node:path';
+import { readFileSync } from 'node:fs';
+
+const ROOT = fileURLToPath(new URL('..', import.meta.url));
+const API = 'https://api.printify.com/v1';
+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;
+}
+async function get(token, path) {
+ const r = await fetch(API + path, { headers: { Authorization: `Bearer ${token}`, 'User-Agent': 'CelebritySignatures/1.0' } });
+ const j = await r.json().catch(() => ({}));
+ if (!r.ok) throw new Error(`Printify ${r.status}: ${j.message || j.error || 'error'} on ${path}`);
+ return j;
+}
+async function main() {
+ const token = envVal('PRINTIFY_API_TOKEN');
+ if (!token) {
+ console.log('PRINTIFY_API_TOKEN not set — generate a scoped personal access token in Printify Profile > Connections, then paste it via the secrets skill.');
+ console.log('This helper is GET-only and will discover shops/products after the token is routed.');
+ return;
+ }
+ const shops = await get(token, '/shops.json');
+ const blueprintArg = process.argv[2];
+ if (!blueprintArg) {
+ console.log(`Printify shops (${shops.length}):`);
+ for (const shop of shops) console.log(` id=${shop.id} ${shop.title} (${shop.sales_channel || 'unknown channel'})`);
+ const blueprints = await get(token, '/catalog/blueprints.json');
+ const candidates = blueprints.filter(b => /t-?shirt|tee|polo/i.test(b.title || ''));
+ console.log(`\nBlank garment blueprints matching tee/polo (${candidates.length}):`);
+ for (const b of candidates.slice(0, 50)) console.log(` id=${b.id} ${b.title}`);
+ console.log('\nNext: node scripts/fetch-printify-catalog.mjs <blueprintId> [printProviderId]');
+ return;
+ }
+ const providerArg = process.argv[3];
+ if (!providerArg) {
+ const providers = await get(token, `/catalog/blueprints/${blueprintArg}/print_providers.json`);
+ console.log(`Print providers for blueprint ${blueprintArg} (${providers.length}):`);
+ for (const p of providers) console.log(` id=${p.id} ${p.title}`);
+ console.log(`\nNext: node scripts/fetch-printify-catalog.mjs ${blueprintArg} <printProviderId>`);
+ return;
+ }
+ const catalog = await get(token, `/catalog/blueprints/${blueprintArg}/print_providers/${providerArg}/variants.json`);
+ const variants = {};
+ for (const v of catalog.variants || []) {
+ const title = String(v.title || '');
+ const size = title.match(/(?:^|\s\/\s)(S|M|L|XL|2XL)(?:$|\s\/\s)/i)?.[1]?.toUpperCase();
+ const color = title.split(/\s\/\s/).find(x => !/^(S|M|L|XL|2XL)$/i.test(x));
+ if (color && size) variants[`${color}/${size}`] = v.id;
+ }
+ console.log(JSON.stringify({ blueprintId: Number(blueprintArg), printProviderId: Number(providerArg),
+ printPosition: { placeholder: 'front', scale: 0.25, x: 0.32, y: 0.3 }, variants }, null, 2));
+ console.log('\nNormalize Printify color labels to our white/heather/sand/sky ids before saving the mapping.');
+}
+main().catch(e => { console.error(e.message); process.exit(1); });
diff --git a/scripts/submit-pod-draft-printify.mjs b/scripts/submit-pod-draft-printify.mjs
new file mode 100644
index 0000000..bfc76f6
--- /dev/null
+++ b/scripts/submit-pod-draft-printify.mjs
@@ -0,0 +1,92 @@
+#!/usr/bin/env node
+// Steve-gated Printify order sender for TK-10286. Printify order creation can
+// enter production depending on account settings, so this refuses to POST unless
+// all FIVE gates are explicit: --apply, token, shop id, live sales, and a manual
+// approval acknowledgement. A provider-specific marker prevents duplicate sends.
+import { readFile, appendFile } from 'node:fs/promises';
+import { readFileSync } from 'node:fs';
+import { fileURLToPath } from 'node:url';
+import { join } from 'node:path';
+import { pathToFileURL } from 'node:url';
+
+const ROOT = fileURLToPath(new URL('..', import.meta.url));
+const DATA = join(ROOT, 'data');
+const API = 'https://api.printify.com/v1';
+const APPLY = process.argv.includes('--apply');
+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;
+}
+function resolveProduct(tpl, d) {
+ const garment = (tpl.garments || []).find(g => g.id === d.garment);
+ if (!garment) throw new Error(`order #${d.orderId}: unknown garment "${d.garment}"`);
+ const map = garment.printify || {};
+ const key = `${d.color}/${d.size}`;
+ if (!map.blueprintId || !map.printProviderId || !map.variants?.[key]) throw new Error(`order #${d.orderId}: ${d.garment} ${key} is not mapped to a Printify blueprint/provider/variant`);
+ return { blueprintId: map.blueprintId, printProviderId: map.printProviderId, variantId: map.variants[key],
+ position: map.printPosition || { placeholder: 'front', scale: 0.25, x: 0.32, y: 0.3 } };
+}
+function splitName(name) {
+ const parts = String(name || '').trim().split(/\s+/).filter(Boolean);
+ return { first_name: parts.shift() || '', last_name: parts.join(' ') || '-' };
+}
+export function buildPayload(tpl, d) {
+ const { blueprintId, printProviderId, variantId, position } = resolveProduct(tpl, d);
+ const r = d.recipient || {};
+ const missing = ['name', 'address1', 'city', 'state_code', 'country_code', 'zip'].filter(k => !r[k]);
+ if (missing.length) throw new Error(`order #${d.orderId}: missing shipping fields ${missing.join(',')}`);
+ if (!d.design_image_url) throw new Error(`order #${d.orderId}: no design_image_url`);
+ const name = splitName(r.name);
+ return {
+ external_id: `celebsig-wear-${d.orderId}`,
+ label: `${d.signature_name} signature — left-chest ${d.garment}`,
+ line_items: [{ print_provider_id: printProviderId, blueprint_id: blueprintId, variant_id: variantId, quantity: 1,
+ external_id: `celebsig-wear-${d.orderId}-1`,
+ print_areas: { [position.placeholder || 'front']: [{ src: d.design_image_url, scale: position.scale,
+ x: position.x, y: position.y, angle: 0 }] } }],
+ is_printify_express: false,
+ is_economy_shipping: false,
+ shipping_method: 1,
+ send_shipping_notification: false,
+ address_to: { ...name, email: d.recipient_email, phone: '', country: r.country_code, region: r.state_code,
+ address1: r.address1, address2: r.address2 || '', city: r.city, zip: r.zip },
+ };
+}
+async function main() {
+ let lines = [];
+ try { lines = (await readFile(join(DATA, 'pod-order-drafts.jsonl'), 'utf8')).trim().split('\n').filter(Boolean); } catch {}
+ const all = lines.map(JSON.parse);
+ const submitted = new Set(all.filter(x => x.status === 'SUBMITTED' && x.provider === 'printify').map(x => x.orderId));
+ const drafts = all.filter(x => x.status === 'DRAFT_UNSENT' && x.provider === 'printify' && !submitted.has(x.orderId));
+ const tpl = JSON.parse(await readFile(join(DATA, 'wear-templates.json'), 'utf8'));
+ const token = envVal('PRINTIFY_API_TOKEN');
+ const shopId = envVal('PRINTIFY_SHOP_ID');
+ const salesLive = envVal('WEAR_SALES_LIVE') === '1';
+ const manual = envVal('PRINTIFY_MANUAL_APPROVAL_CONFIRMED') === '1';
+ const willApply = APPLY && !!token && !!shopId && salesLive && manual;
+ console.log(`POD drafts pending: ${drafts.length} (provider: Printify)`);
+ for (const d of drafts) {
+ let payload;
+ try { payload = buildPayload(tpl, d); } catch (e) { console.log(` SKIP: ${e.message}`); continue; }
+ console.log(JSON.stringify(payload, null, 2));
+ if (!willApply) continue;
+ const resp = await fetch(`${API}/shops/${shopId}/orders.json`, { method: 'POST',
+ headers: { Authorization: `Bearer ${token}`, 'User-Agent': 'CelebritySignatures/1.0', 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload) });
+ const body = await resp.json().catch(() => ({}));
+ if (!resp.ok) { console.error(`PRINTIFY ERROR ${resp.status}: ${body.message || body.error || 'unknown'}`); continue; }
+ await appendFile(join(DATA, 'pod-order-drafts.jsonl'), JSON.stringify({ orderId: d.orderId, status: 'SUBMITTED', provider: 'printify', printifyId: body.id, submittedAt: new Date().toISOString() }) + '\n');
+ console.log(` Printify order created: ${body.id}; production remains controlled by the account's manual approval setting.`);
+ }
+ if (!willApply) {
+ console.log('\nDRY RUN — nothing submitted to Printify.');
+ console.log(` --apply: ${APPLY} | token: ${!!token} | shop: ${!!shopId} | sales live: ${salesLive} | manual approval confirmed: ${manual}`);
+ console.log(' All five gates must be true. Verify the Printify store Order approval setting is Manual before setting the acknowledgement.');
+ }
+}
+if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
+ main().catch(e => { console.error(e.message); process.exit(1); });
+}
diff --git a/scripts/submit-pod-draft.mjs b/scripts/submit-pod-draft.mjs
index e22faf6..8bdf75c 100644
--- a/scripts/submit-pod-draft.mjs
+++ b/scripts/submit-pod-draft.mjs
@@ -76,8 +76,8 @@ async function main() {
const all = lines.map(l => JSON.parse(l));
// Orders already sent to Printful get a SUBMITTED marker line appended below.
// Exclude them so a rerun (network glitch, human re-run) NEVER double-submits.
- const submitted = new Set(all.filter(d => d.status === 'SUBMITTED').map(d => d.orderId));
- const drafts = all.filter(d => d.status === 'DRAFT_UNSENT' && !submitted.has(d.orderId));
+ const submitted = new Set(all.filter(d => d.status === 'SUBMITTED' && (!d.provider || d.provider === 'printful')).map(d => d.orderId));
+ const drafts = all.filter(d => d.status === 'DRAFT_UNSENT' && (!d.provider || d.provider === 'printful') && !submitted.has(d.orderId));
const tpl = await templates();
console.log(`POD drafts pending: ${drafts.length} (provider: Printful)`);
@@ -101,7 +101,7 @@ async function main() {
const j = await resp.json().catch(() => ({}));
if (!resp.ok) { console.error(` PRINTFUL ERROR ${resp.status}: ${j.error?.message || j.result || 'unknown'}`); continue; }
// Mark this order SUBMITTED so a rerun skips it (append-only, matches the draft log).
- await appendFile(join(DATA, 'pod-order-drafts.jsonl'), JSON.stringify({ orderId: d.orderId, status: 'SUBMITTED', printfulId: j.result?.id, submittedAt: new Date().toISOString() }) + '\n');
+ await appendFile(join(DATA, 'pod-order-drafts.jsonl'), JSON.stringify({ orderId: d.orderId, status: 'SUBMITTED', provider: 'printful', printfulId: j.result?.id, submittedAt: new Date().toISOString() }) + '\n');
console.log(` ✓ Printful DRAFT order created: id=${j.result?.id} status=${j.result?.status} (confirm it in the Printful dashboard to fulfill).`);
}
diff --git a/server.js b/server.js
index fcaa47f..202f51d 100644
--- a/server.js
+++ b/server.js
@@ -153,6 +153,21 @@ async function wearTemplates() {
_wearTplAt = Date.now();
return _wearTplCache;
}
+const WEAR_POD_PROVIDERS = new Set(['printful', 'printify']);
+function wearPodProvider() {
+ const provider = String(envVal('WEAR_POD_PROVIDER') || 'printful').toLowerCase();
+ return WEAR_POD_PROVIDERS.has(provider) ? provider : null;
+}
+function wearPodReady(tpl, provider) {
+ if (!provider) return false;
+ const credential = provider === 'printify' ? envVal('PRINTIFY_API_TOKEN') : envVal('PRINTFUL_API_KEY');
+ if (!credential) return false;
+ return (tpl.garments || []).every(g => {
+ const mapping = g[provider] || {};
+ if (provider === 'printify' && (!mapping.blueprintId || !mapping.printProviderId)) return false;
+ return (g.colors || []).every(c => (g.sizes || []).every(size => mapping.variants?.[`${c.id}/${size}`]));
+ });
+}
// v1 (2026-08-10, Steve GO — TK-10286): the USPTO live-trademark auto-screen is DEFERRED.
// A signature is sellable on apparel only if it is a clearly PUBLIC-DOMAIN HISTORICAL figure
// — deceased on/before WEAR_PD_DEATH_CUTOFF — where there is no live apparel-trademark or
@@ -791,6 +806,11 @@ ${paid ? `<div class="ok">✓</div><h1>Order confirmed</h1>
// flips WEAR_SALES_LIVE=1 (paired with live Stripe + a wired POD). Until then, no visitor
// is sent to a test-mode checkout — they get a friendly "opening soon".
if (envVal('WEAR_SALES_LIVE') !== '1') return sendJSON(res, 200, { ok: false, comingSoon: true, error: 'Apparel sales open soon — thanks for your interest!' });
+ // Never charge a customer unless the selected POD provider has both a
+ // credential and a complete color/size mapping for every offered garment.
+ const podProvider = wearPodProvider();
+ const podTemplates = await wearTemplates();
+ if (!wearPodReady(podTemplates, podProvider)) return sendJSON(res, 503, { ok: false, error: 'apparel fulfillment is not configured' });
// Apparel is a physical POD sale — charge through the live-capable resolver
// (STRIPE_MURAL_KEY: live only when STRIPE_LIVE_ENABLED=1 AND a sk_live_ key
// is present, else the test key). NOT STRIPE_DOWNLOAD_KEY, which is test-only,
@@ -802,7 +822,7 @@ ${paid ? `<div class="ok">✓</div><h1>Order confirmed</h1>
const clr = await tmClearance();
const sig = (await mergedSignatures()).find(s => qidOf(s) === String(b.qid || ''));
if (!sig || !wearEligible(sig, clr)) return sendJSON(res, 403, { ok: false, error: 'that signature is not cleared for sale' });
- const tpl = await wearTemplates();
+ const tpl = podTemplates;
const garment = (tpl.garments || []).find(g => g.id === String(b.garment || ''));
if (!garment) return sendJSON(res, 400, { ok: false, error: 'unknown garment' });
const color = (garment.colors || []).find(c => c.id === String(b.color || '')) || garment.colors[0];
@@ -863,16 +883,15 @@ ${paid ? `<div class="ok">✓</div><h1>Order confirmed</h1>
const ad = sd.address || {};
order.recipient = { name: sd.name || null, address1: ad.line1 || null, address2: ad.line2 || null,
city: ad.city || null, state_code: ad.state || null, country_code: ad.country || null, zip: ad.postal_code || null };
- // DRAFT the POD order — do NOT submit. This is the exact payload a
- // future Printful call will send; scripts/submit-pod-draft.mjs
- // is the Steve-gated sender (Printful, confirm=false drafts).
+ // DRAFT the POD order — do NOT submit. The selected provider's
+ // separately gated sender validates and transmits it later.
if (!order.pod_submitted) {
const sig = (await mergedSignatures()).find(x => qidOf(x) === order.qid);
- const draft = { draftedAt: new Date().toISOString(), orderId: order.id, status: 'DRAFT_UNSENT', provider: 'printful',
+ const draft = { draftedAt: new Date().toISOString(), orderId: order.id, status: 'DRAFT_UNSENT', provider: wearPodProvider(),
recipient_email: order.email, recipient: order.recipient, garment: order.garment, color: order.color, size: order.size,
placement: order.placement, signature_name: order.signature_name,
design_image_url: sig ? sig.signature_image_url : null,
- note: 'NOT SENT — awaiting Steve approval + PRINTFUL_API_KEY + WEAR_SALES_LIVE=1 (scripts/submit-pod-draft.mjs --apply)' };
+ note: `NOT SENT — awaiting Steve approval + ${wearPodProvider()} credentials/mapping + WEAR_SALES_LIVE=1` };
await appendFile(join(DATA, 'pod-order-drafts.jsonl'), JSON.stringify(draft) + '\n');
order.pod_submitted = true;
}
diff --git a/test/printify-pod.test.mjs b/test/printify-pod.test.mjs
new file mode 100644
index 0000000..b458653
--- /dev/null
+++ b/test/printify-pod.test.mjs
@@ -0,0 +1,33 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import { buildPayload } from '../scripts/submit-pod-draft-printify.mjs';
+
+const template = { garments: [{ id: 'classic-tee', printify: {
+ blueprintId: 9, printProviderId: 5,
+ printPosition: { placeholder: 'front', scale: 0.25, x: 0.32, y: 0.3 },
+ variants: { 'white/2XL': 17887 },
+} }] };
+const draft = { orderId: 5001, garment: 'classic-tee', color: 'white', size: '2XL',
+ signature_name: 'Thomas Jefferson', design_image_url: 'https://example.com/signature.png',
+ recipient_email: 'proof@example.com', recipient: { name: 'Test Buyer', address1: '1 Main St',
+ city: 'Los Angeles', state_code: 'CA', country_code: 'US', zip: '90001' } };
+
+test('builds a dynamic Printify left-chest order payload', () => {
+ const payload = buildPayload(template, draft);
+ assert.equal(payload.line_items[0].blueprint_id, 9);
+ assert.equal(payload.line_items[0].print_provider_id, 5);
+ assert.equal(payload.line_items[0].variant_id, 17887);
+ assert.deepEqual(payload.line_items[0].print_areas.front, [{
+ src: draft.design_image_url, scale: 0.25, x: 0.32, y: 0.3, angle: 0,
+ }]);
+ assert.equal(payload.send_shipping_notification, false);
+});
+
+test('fails loud on unmapped variants', () => {
+ assert.throws(() => buildPayload(template, { ...draft, size: 'XL' }), /not mapped/);
+});
+
+test('fails loud without a design or complete shipping address', () => {
+ assert.throws(() => buildPayload(template, { ...draft, design_image_url: null }), /no design_image_url/);
+ assert.throws(() => buildPayload(template, { ...draft, recipient: { ...draft.recipient, zip: '' } }), /missing shipping fields zip/);
+});
diff --git a/verification/e2e-proof-printify.json b/verification/e2e-proof-printify.json
new file mode 100644
index 0000000..9d36cf5
--- /dev/null
+++ b/verification/e2e-proof-printify.json
@@ -0,0 +1,46 @@
+{
+ "intent": "Add a safely disabled Printify fulfillment option for TK-10286 while preserving the existing Printful path and preventing charges without fulfillment readiness.",
+ "riskTier": "R2 local user-facing and order-boundary change; live provider and money actions remain gated.",
+ "environment": "Local CelebritySignatures servers on ports 19921 and 19922; no Printify token, shop id, variant mapping, or live provider action.",
+ "baselineCommit": "4f0db6e2bf3a871de6fc88af62f0364b7c8c4aae",
+ "timestamp": "2026-09-03T20:54:52Z",
+ "assertions": [
+ {
+ "boundary": "unit/contract",
+ "check": "Dynamic Printify payload uses blueprint_id, print_provider_id, selected variant_id, and the paid order's signature image at front x=0.32, y=0.30, scale=0.25; missing mapping, artwork, or address fails loud.",
+ "verdict": "PASS",
+ "evidence": "node --test test/printify-pod.test.mjs: 3/3 passed"
+ },
+ {
+ "boundary": "provider sender",
+ "check": "Without credentials the Printify sender reports zero pending and DRY RUN. POST requires --apply, PRINTIFY_API_TOKEN, PRINTIFY_SHOP_ID, WEAR_SALES_LIVE=1, and PRINTIFY_MANUAL_APPROVAL_CONFIRMED=1.",
+ "verdict": "PASS"
+ },
+ {
+ "boundary": "payment negative path",
+ "check": "With WEAR_SALES_LIVE=1 and WEAR_POD_PROVIDER=printify artificially set, but no token/mappings, checkout returns HTTP 503 apparel fulfillment is not configured before any Stripe or order write.",
+ "verdict": "PASS",
+ "evidence": "data/wear-orders.json SHA-1 remained dc87075a6a70b90c42723986b372eed8df7fc6f3 before and after"
+ },
+ {
+ "boundary": "UI regression",
+ "check": "/wear rendered 2955 cards; opening a card showed the left-chest preview; zero JavaScript errors; HTTP, headless paint, Playwright interaction, and installed Chrome passed.",
+ "verdict": "PASS",
+ "artifact": "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/3x-99EGa9"
+ },
+ {
+ "boundary": "cross-browser",
+ "check": "Safari and Firefox automation were unavailable with --no-open.",
+ "verdict": "SKIP",
+ "reason": "Non-critical parity check; four independent installed layers passed."
+ },
+ {
+ "boundary": "external account",
+ "check": "Real Printify shop/catalog discovery and shipping/order validation.",
+ "verdict": "SKIP",
+ "reason": "PRINTIFY_API_TOKEN is not stored; this is a critical activation prerequisite, so live readiness is not claimed."
+ }
+ ],
+ "cleanup": "Temporary local servers stopped. No Stripe session, local order, Printify request, production job, deployment, publish, DNS change, or remote push occurred.",
+ "overallVerdict": "PASS for disabled adapter and fail-closed checkout. PARTIAL/BLOCKED for real account integration until Steve provides or authorizes creation of a scoped Printify personal access token and explicitly approves later live activation."
+}
← 4f0db6e document TK-10286 wear end-to-end proof
·
back to CelebritySignatures
·
Celebrity Signatures: correct a FALSE 'Data Not Collected' p 919eed1 →