[object Object]

← back to Pattern Vault

wpb-uploaders: Etsy createDraftListing payload mapping + --dry-run (38 payloads, gated on OAuth+taxonomy); per-platform STATUS.md

6662111a27300e6090e3250c579fcf071073d105 · 2026-07-09 18:14:49 -0700 · Steve

Files touched

Diff

commit 6662111a27300e6090e3250c579fcf071073d105
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Jul 9 18:14:49 2026 -0700

    wpb-uploaders: Etsy createDraftListing payload mapping + --dry-run (38 payloads, gated on OAuth+taxonomy); per-platform STATUS.md
---
 wpb-uploaders/STATUS.md         | 23 ++++++++++++++
 wpb-uploaders/platforms/etsy.js | 66 ++++++++++++++++++++++++++++++++++++-----
 wpb-uploaders/run.js            |  4 +++
 3 files changed, 85 insertions(+), 8 deletions(-)

diff --git a/wpb-uploaders/STATUS.md b/wpb-uploaders/STATUS.md
new file mode 100644
index 0000000..348b036
--- /dev/null
+++ b/wpb-uploaders/STATUS.md
@@ -0,0 +1,23 @@
+# WPB Uploaders — build status (2026-07-09)
+
+Honest state of each platform. NONE is armed. Real uploads flow only after the per-platform
+blocker clears AND Steve arms it (`touch platforms/<key>.ARMED`).
+
+| Platform | Code state | Exact blocker to arming | Type |
+|----------|-----------|--------------------------|------|
+| **Spoonflower** (Fernwick) | fail-loud ✓, filechooser ✓, consent net-block + iframe-dismiss ✓ | **File doesn't register post-filechooser** — the "Choose Files" native chooser fires and `setFiles` runs, but the upload never starts (stays on the empty `/new` form, no preview/Save). Needs a **HEADED watch session** to see the post-select state (preview? validation reject? anti-automation no-op?). | web (armed) |
+| **Etsy** (SteveAbramsStudios) | `createDraftListing` payload mapping ✓ (38 built, `--dry-run` proves it) | **(1) ETSY_OAUTH_TOKEN** (Steve-gated OAuth for the shop); **(2) taxonomy_id unresolved** (`plan.taxonomyIdResolved` is null — resolve via getSellerTaxonomyNodes once authed) | API |
+| **Creative Market** | disarmed stub (navigates to upload URL, fail-loud) | **Session capture** — need Steve to log in once → `sessions/creativemarket-state.json`; then live DOM calibration | web |
+| **Redbubble** | disarmed stub, fail-loud | **Session capture** + calibration; **AI-hostile** → anti-bot cadence critical (ban risk) | web |
+| **Society6** | disarmed stub, fail-loud | **Account still PENDING confirmation** — cannot even session-capture yet | web |
+| **Patternbank** | disarmed stub, fail-loud | **Account still PENDING confirmation** — cannot even session-capture yet | web |
+
+## What's autonomously reversible from here
+- Spoonflower: nothing more headless — needs the headed session (Steve or a watched run).
+- Etsy: once Steve provides OAuth, wire the real `fetch` POST behind the token check + resolve taxonomy — the mapping is done.
+- CM/Redbubble: once Steve captures a session, calibrate each `uploadFn` against the live DOM.
+- Society6/Patternbank: blocked on account confirmation — nothing to do until then.
+
+## Standing safeguards (never loosened)
+Fail-loud (real id or `failed[]`), ≤4/day cap, human-typed fields, dry-run-unless-`.ARMED`,
+for-sale stays OFF until payout/tax per platform. Arming is drafted to `~/.claude/yolo-queue/pending-approval/`.
diff --git a/wpb-uploaders/platforms/etsy.js b/wpb-uploaders/platforms/etsy.js
index 84cb74d..43fe876 100644
--- a/wpb-uploaders/platforms/etsy.js
+++ b/wpb-uploaders/platforms/etsy.js
@@ -1,17 +1,67 @@
 /* Etsy uploader — brand: Wallpaper's Back · shop SteveAbramsStudios
- * DISARMED. Etsy is API-based (v3 createDraftListing) — NOT a Playwright web upload.
+ * Etsy is API-based (v3 createDraftListing) — NOT a Playwright web upload.
  * The listing plan already exists at pattern-vault/data/etsy-listings-plan.json (38 planned,
- * price $149 digital download). Arming = obtaining Etsy OAuth for the shop + pushing drafts.
- * This module is API-shaped, so base.js's Playwright driver is bypassed for Etsy (see run.js). */
-const path = require('path'), os = require('os');
+ * digital download @ $149). This module BUILDS the createDraftListing payloads from that plan
+ * and can print them (--dry-run, $0, no network). The live POST is gated behind ETSY_OAUTH_TOKEN
+ * (Steve-gated) AND a resolved taxonomy_id + shop_id.
+ */
+const path = require('path'), os = require('os'), fs = require('fs');
+
+const PLAN = path.join(os.homedir(), 'Projects/pattern-vault/data/etsy-listings-plan.json');
+
+// Map one plan entry + the plan's shared `fixed` block → an Etsy v3 createDraftListing payload.
+function toPayload(entry, fixed, taxonomyId) {
+  const b = entry.body || {};
+  return {
+    quantity: b.quantity || fixed.quantity || 999,
+    title: (b.title || '').slice(0, 140),            // Etsy title max 140 chars
+    description: b.description || '',
+    price: fixed.price,                               // $149 digital
+    who_made: fixed.who_made || 'i_did',
+    when_made: fixed.when_made || '2020_2026',
+    taxonomy_id: taxonomyId || null,                 // MUST be resolved before live POST
+    type: fixed.type || 'download',                  // digital download
+    is_supply: false,
+    is_digital: true,
+    state: 'draft',                                  // ALWAYS draft — never auto-publish
+    _imageFile: entry.imageFile || null,             // uploaded separately via uploadListingImage
+  };
+}
+
+function buildPayloads() {
+  if (!fs.existsSync(PLAN)) return { error: `no plan file at ${PLAN}` };
+  const plan = JSON.parse(fs.readFileSync(PLAN, 'utf8'));
+  const fixed = plan.fixed || {};
+  const taxonomyId = plan.taxonomyIdResolved || null;
+  const payloads = (plan.plan || []).map(e => toPayload(e, fixed, taxonomyId));
+  return { count: payloads.length, taxonomyResolved: !!taxonomyId, price: fixed.price, payloads };
+}
+
 module.exports = {
   key: 'etsy', name: 'Etsy', brand: "Wallpaper's Back", apiBased: true,
   shop: 'SteveAbramsStudios',
-  planFile: path.join(os.homedir(), 'Projects/pattern-vault/data/etsy-listings-plan.json'),
+  planFile: PLAN,
   endpoint: 'POST /v3/application/shops/{shop_id}/listings (createDraftListing)',
-  // returns {id,url} per created draft, else {reason}. Needs ETSY_OAUTH_TOKEN + shop_id.
+  buildPayloads,
+
+  // dryRun: print the payloads we WOULD send (proves the mapping, $0, no network).
+  dryRun() {
+    const built = buildPayloads();
+    if (built.error) return built;
+    return {
+      dryRun: true, count: built.count, price: built.price,
+      taxonomyResolved: built.taxonomyResolved,
+      blockers: [
+        !process.env.ETSY_OAUTH_TOKEN && 'ETSY_OAUTH_TOKEN missing (Steve-gated OAuth)',
+        !built.taxonomyResolved && 'taxonomy_id unresolved (plan.taxonomyIdResolved is null)',
+      ].filter(Boolean),
+      sample: built.payloads.slice(0, 2),
+    };
+  },
+
+  // live path — DISARMED: refuses without a token; even with one, the POST stays Steve-gated.
   async uploadFn() {
-    if (!process.env.ETSY_OAUTH_TOKEN) return { reason: 'etsy DISARMED — no ETSY_OAUTH_TOKEN; obtain shop OAuth then push drafts from etsy-listings-plan.json' };
-    return { reason: 'etsy uploadFn not yet wired — DISARMED (token present but draft-push not enabled; Steve-gated)' };
+    if (!process.env.ETSY_OAUTH_TOKEN) return { reason: 'etsy DISARMED — no ETSY_OAUTH_TOKEN; run `node run.js etsy --dry-run` to preview payloads' };
+    return { reason: 'etsy DISARMED — token present but live createDraftListing POST is Steve-gated (arm via pending-approval)' };
   },
 };
diff --git a/wpb-uploaders/run.js b/wpb-uploaders/run.js
index 07026dd..26fd846 100644
--- a/wpb-uploaders/run.js
+++ b/wpb-uploaders/run.js
@@ -32,6 +32,10 @@ function loadPlatform(key) { return require(path.join(__dirname, 'platforms', `$
   const manifest = manifestArg || path.join(require('os').homedir(), 'Projects/pattern-vault/data/catalog.json');
 
   if (cfg.apiBased) { // Etsy path — no browser
+    if (process.argv.includes('--dry-run') && cfg.dryRun) {
+      console.log(JSON.stringify({ platform: cfg.key, apiBased: true, ...cfg.dryRun() }, null, 2));
+      return;
+    }
     const r = await cfg.uploadFn();
     console.log(JSON.stringify({ platform: cfg.key, apiBased: true, result: r }, null, 2));
     return;

← 7fb0d68 spoonflower: proven filechooser selector + network-block con  ·  back to Pattern Vault  ·  SPOONFLOWER UPLOAD WORKS (real id 22723313 proven): late-con f72c062 →