← back to Designer Wallcoverings
CLAUDE.md
974 lines
- all vendors need this fix so we are always savinf data #memory
- **CRITICAL DATABASE**: ALWAYS use `dw_unified` database - NEVER create new databases! Connection string lives in `.env` (DATABASE_URL); use `dw_admin@127.0.0.1:5432/dw_unified`. NEVER commit credentials to docs.
## Shopify Plus — 100K+ Product Push Strategy
### Why Plus (vs Standard)
| Capability | Standard | Plus |
|---|---|---|
| REST req/s | 2/s (40 bucket) | 20/s (400 bucket) — **10x** |
| GraphQL pts/s | 100 pts/s | 1,000 pts/s — **10x** |
| Daily variant limit | 1,000/day (>50K variants) | **REMOVED** |
| Bulk mutations concurrent | 1 | 5 (API 2026-01+) |
| Shopify Flow | No | Yes |
| Launchpad (scheduled publishes) | No | Yes |
| B2B / wholesale | No | Yes |
| Price | — | $2,300-$2,500/mo |
### Migration: Same-store upgrade, ~24 hours, no data migration. SEO preserved.
### Bulk Operation Pipeline (100K products)
1. **Generate JSONL** — one JSON object per product line, max 100MB per file (~30K-50K products)
2. **stagedUploadsCreate** — reserve upload space (resource: BULK_MUTATION_VARIABLES, mime: text/jsonl)
3. **Upload JSONL** — POST multipart form to staged URL
4. **bulkOperationRunMutation** — submit async (NOT rate-limited)
5. **Poll or webhook** — BULK_OPERATION_FINISH webhook fires on completion
6. **Results** — JSONL download available 7 days
### Time Estimates for 100K Products on Plus
| Method | Time |
|---|---|
| REST API (20 req/s) | 5-10 hours |
| GraphQL single mutations | 2-5 hours |
| GraphQL bulk ops (1 concurrent) | 30 min - 3 hours |
| GraphQL bulk ops (5 parallel, 2026-01) | **10 min - 1 hour** |
### Image Strategy: Use external CDN URLs in JSONL `images: [{src: "https://..."}]`. Shopify fetches async. Do NOT pre-stage 100K images individually.
### Constraints
- JSONL max: 100MB per file
- Max operation runtime: 24 hours
- Max images per product: 250
- Single query cap: 1,000 points (all plans)
- Results available: 7 days
## CRITICAL: Discontinued Products → ARCHIVE (Never Draft, Never Delete) #memory
**When a vendor discontinues a product, ARCHIVE it on Shopify. Never draft, never delete.**
### Rules
1. **Discontinued = ARCHIVE** — Set `status: "archived"` via Shopify Admin API
2. **Never use draft** for discontinued products — draft is for unpublished new products
3. **Never delete** discontinued products — archived products preserve SEO and order history
4. **Detection**: If vendor product URL returns 301/404/redirect to category page → product is discontinued
5. **Update both**: Set `status: 'archived'` in PostgreSQL catalog table AND Shopify
### API Call
```javascript
// Archive a discontinued product on Shopify
PUT /admin/api/2024-01/products/{product_id}.json
{ "product": { "id": product_id, "status": "archived" } }
```
## CRITICAL: NEVER Use "Unknown" as a Pattern Name #memory
**🚫 "UNKNOWN" MUST NEVER APPEAR IN A SHOPIFY PRODUCT TITLE — EVER!**
### Rules
1. **If no pattern name is found from scraping** — do NOT set pattern to "Unknown"
2. **If pattern name cannot be determined** — use the MFR SKU as the pattern name instead (e.g. `T10958 Blue | Thibaut`)
3. **If MFR SKU is also missing** — use only the color name (e.g. `Blue | Thibaut`)
4. **NEVER publish a product with "Unknown" in the title** — block it from the schedule
5. **Always scrape vendor website FIRST** and save pattern name to PostgreSQL BEFORE setting Shopify title
6. **Tag products for review** if pattern name cannot be found — add `Needs-Pattern-Name` tag
### Fallback Order for Pattern Name
1. Vendor website scrape (thibautdesign.com, koroseal.com, etc.)
2. Spreadsheet data (TAK, Type II Vinyl, etc.)
3. Metafields from Shopify (global.Pattern, custom.name_of_pattern)
4. MFR SKU as pattern name (last resort before "Unknown")
5. **NEVER "Unknown"** — skip the pattern entirely if nothing found
### Validation Check (MANDATORY before Shopify title update)
```javascript
function validateTitle(title) {
if (!title || title.toLowerCase().includes('unknown')) {
throw new Error('BLOCKED: Title contains "Unknown" - fix pattern name first');
}
return title;
}
```
## CRITICAL: Sample Variant Required for ALL Products #memory
**🏷️ EVERY PRODUCT MUST HAVE A "SAMPLE" VARIANT - NO EXCEPTIONS!**
### Sample Variant Requirements
| Field | Value | Notes |
|-------|-------|-------|
| **Title** | `Sample` | Always "Sample" |
| **SKU** | `{DW_SKU}-Sample` | DW SKU + "-Sample" suffix |
| **Price** | `$4.25` | Standard sample price for most vendors |
| **Inventory** | `null` | No inventory tracking |
### DW SKU Assignment Rules
1. **Assign at FIRST IMPORT ONLY** - Never change once assigned
2. **Format**: `DW{XX}-{NUMBER}` — DW + 2 random letters + dash + sequential number
3. **NEVER reference vendor name** in prefix — too easy to identify vendor
4. **NEVER use numbers** in prefix — letters only
5. **FORBIDDEN letters** (hard to hear on phone): **B, D, S, F, M, N, V**
6. **ALLOWED letters**: A, C, E, G, H, I, J, K, L, O, P, Q, R, T, U, W, X, Y, Z (19 chars)
7. Each vendor gets a **unique** 2-letter code from allowed set
8. **VCC auto-generates compliant prefixes** via `generateCompliantPrefix()` on first SKU assignment
9. **Sequential numbering** - Each vendor's range starts 5000 ahead of the last
10. **Store in PostgreSQL** - Save to vendor catalog table `dw_sku` column
11. **Legacy prefixes** (DWSC, DWTT, etc.) — leave existing, only apply new rules going forward
### Product Variant Structure (MANDATORY)
```javascript
variants: [
{
title: 'Sample',
sku: `${dwSku}-Sample`, // e.g., "DWK-040251-Sample"
price: '4.25',
inventory_management: null
}
]
```
### Implementation in Shopify Product Creation
```javascript
// Generate DW SKU FIRST
const dwSku = await generateDwSku(vendorPrefix, vendorName, mfrSku);
// Create product with Sample variant
const productData = {
product: {
title: productTitle,
variants: [{
title: 'Sample',
sku: `${dwSku}-Sample`,
price: '4.25',
inventory_management: null
}],
// ... other fields
}
};
```
### Why This Matters
- Customers need to order samples before full rolls
- DW SKU provides unique identifier across all vendors
- Sample pricing is standardized at $4.25
- SKU format enables easy tracking and reordering
---
## CRITICAL: Never Activate SKU Without Specs + Description + ALL Images #memory
**🚫 NEVER set a Shopify product to `status: "active"` unless ALL of these hold**
(extended 2026-06-20 from the old width+image rule to a fuller gate):
1. **SPECS** — `global.width` (or `custom.width`) set + non-empty (HARD-required),
PLUS the core set (length, repeat, content/material, unit_of_measure) **where
the vendor provides them** (don't block on a spec the vendor genuinely lacks).
2. **DESCRIPTION** — a non-empty `body_html`/description that is NOT a placeholder,
NOT "Unknown"/"Page Not Found"/404/error, and NOT bare legal/disclaimer text.
3. **IMAGES** — ≥1 product image AND **all** of the vendor's available images for
that SKU attached (`vendor_catalog.all_images` / `image_url`; full-page-scrape).
4. **GUARDS** — no banned word "Wallpaper" in title, no "Unknown" in title, sample
variant present (`{DW_SKU}-Sample`).
**Single enforced validator** (use this, don't reinvent):
`shopify/scripts/lib/validate-before-activate.js` → `validateBeforeActivate(product)`.
Wired into BOTH activation chokepoints — `cadence/cadence-import.js` (create-time)
and `cadence/activate-gated.js` (DRAFT→ACTIVE promote). On fail → keep DRAFT +
tag `Needs-Specs` / `Needs-Description` / `Needs-Image`.
Legacy summary (still true, subset of the above):
1. **A width metafield** — `global.width` or `custom.width` must be set (not null/empty)
2. **At least one product image** — `product.images.length >= 1`
### Rules
1. **Products missing width OR image remain as `draft`** — do not activate them
2. **Check BEFORE any status change** — validate both fields before calling Shopify API
3. **Bulk imports**: Skip activation for products that fail this check; tag them `Needs-Width` or `Needs-Image`
4. **Width sources** (in priority order): vendor website scrape, spreadsheet data, metafield sync
5. **Image sources**: vendor CDN, uploaded images, room settings
### Validation Code (MANDATORY before activation)
```javascript
function canActivateProduct(product) {
const hasImage = product.images && product.images.length > 0;
const hasWidth = product.metafields?.some(m =>
(m.namespace === 'global' || m.namespace === 'custom') &&
m.key === 'width' && m.value && m.value.trim() !== ''
);
if (!hasImage) console.warn(`BLOCKED: ${product.title} has no images`);
if (!hasWidth) console.warn(`BLOCKED: ${product.title} has no width metafield`);
return hasImage && hasWidth;
}
```
### Why This Matters
- Products without width confuse trade buyers who need to calculate coverage
- Products without images get zero engagement and hurt SEO
- Better to keep as draft until complete than show incomplete listings
---
## CRITICAL: SKU Registry for New Product Imports #memory
**🔢 BEFORE CREATING ANY NEW PRODUCT IN SHOPIFY - CHECK FOR DUPLICATES!**
### Import the SKU Registry Module
```javascript
const { checkDuplicate, registerSku, updateShopifyInfo } = require('./lib/sku-registry');
// Located: /root/Projects/Designer-Wallcoverings/shopify/scripts/lib/sku-registry.js
```
### Mandatory Workflow for New Products
```javascript
async function importNewProduct(vendorPrefix, vendorName, mfrSku, productData) {
// Step 1: CHECK for duplicate
const dup = await checkDuplicate(vendorPrefix, mfrSku);
if (dup.exists) {
console.log(`⚠️ SKIP: ${mfrSku} already exists as ${dup.existingSku}`);
return null;
}
// Step 2: REGISTER before Shopify creation
const dwSku = await registerSku(vendorPrefix, vendorName, mfrSku);
if (!dwSku) return null;
// Step 3: CREATE in Shopify with DW SKU
const shopifyProduct = await createShopifyProduct({ ...productData, sku: dwSku });
// Step 4: UPDATE registry with Shopify info
await updateShopifyInfo(dwSku, shopifyProduct.id, shopifyProduct.handle);
return { dwSku, shopifyId: shopifyProduct.id };
}
```
### Vendor Prefix Quick Reference
| Prefix | Vendor | Prefix | Vendor |
|--------|--------|--------|--------|
| DWK- | Koroseal | DWMR- | Maya Romanoff |
| DWC-Arte- | Arte | DWCC- | Novasuede |
| DWTT- | Thibaut | DWKK- | Kravet |
| DWRR- | Ralph Lauren | DWSC- | Schumacher |
| DWJS- | York | DWPJ- | Phillip Jeffries |
| DWWC- | Default/Generic | | |
**Database**: `dw_unified` → Table: `dw_sku_registry`
---
## CRITICAL: AI Analysis MUST Be Saved (Analyze + Accept) #memory
**🎨 AI ANALYSIS REQUIRES TWO STEPS - ANALYZE THEN ACCEPT!**
The `/api/analyze-image` endpoint only RETURNS analysis - it does NOT save to database!
You MUST call `/api/accept-analysis` to actually SAVE the results.
### API Endpoints (Arte Dashboard - Port 9955)
| Endpoint | Purpose | Saves to DB? |
|----------|---------|--------------|
| `POST /api/analyze-image` | Run Gemini AI analysis on image | ❌ NO - Returns only |
| `POST /api/accept-analysis` | Save analysis to PostgreSQL | ✅ YES - Saves data |
| `POST /api/scrape-arte` | Fetch MFR data from Arte website | ❌ NO - Returns only |
### Complete AI Analysis Workflow
```bash
AUTH="Basic $(printf '%s' \"$ADMIN_USER:$ADMIN_PASS\" | base64)" # creds via env — see /secrets
API="http://localhost:9955"
# Step 1: AI Analyze (returns but does NOT save)
AI_RESULT=$(curl -s -X POST "$API/api/analyze-image" \
-H "Authorization: $AUTH" \
-H "Content-Type: application/json" \
-d '{"imageUrl":"IMAGE_URL","title":"Pattern Color"}')
# Step 2: Extract FULL analysis object
ANALYSIS=$(echo "$AI_RESULT" | jq '.analysis')
# Step 3: SAVE to database - aiAnalysis MUST be nested object!
curl -s -X POST "$API/api/accept-analysis" \
-H "Authorization: $AUTH" \
-H "Content-Type: application/json" \
-d "{\"arteSku\":\"$SKU\",\"aiAnalysis\":$ANALYSIS}"
```
**⚠️ CRITICAL: The `aiAnalysis` parameter MUST be a nested object containing:**
- `colors` (array)
- `tags` (array)
- `description` (string)
- `backgroundColor` (string)
- `styles` (array)
- `patterns` (array)
### Database Columns Updated by Accept
| Column | Type | Description |
|--------|------|-------------|
| `ai_colors` | text[] | Array of detected colors |
| `ai_tags` | text[] | Interior design tags |
| `ai_description` | text | 2-sentence commercial description |
| `ai_background_color` | text | Dominant background color |
| `ai_styles` | text[] | Style periods (Contemporary, Art Deco, etc.) |
| `ai_patterns` | text[] | Pattern types (Geometric, Floral, etc.) |
### Gemini 3.0 Prompt (Interior Design Tagger)
```javascript
const GEMINI_API_KEY = process.env.GEMINI_API_KEY /* 2026-05-05 SCRUB: was hardcoded; rotate via /secrets */;
// NEVER put example values in prompt - AI will return them!
// BAD: "backgroundColor": "Navy"
// GOOD: "backgroundColor": "<actual color you see>"
```
### Scheduling Products (Calendar API - Port 9958)
```bash
CAL="http://localhost:9958"
curl -s -X POST "$CAL/api/schedule" \
-H "Authorization: $AUTH" \
-H "Content-Type: application/json" \
-d '{
"productId": "SKU",
"productTitle": "Pattern Color",
"vendorChannelId": 1,
"scheduledDate": "2026-02-04T12:00:00",
"imageUrl": "IMAGE_URL",
"mfrSku": "SKU"
}'
```
### UI Card Turns Purple When Scheduled
The card at http://45.61.58.125:9660/ automatically turns purple when scheduled:
```javascript
background: scheduled ? 'linear-gradient(135deg, #7c3aed 0%, #6d28d9 100%)' : 'white'
```
## CRITICAL: PostgreSQL BEFORE Shopify #memory
**🔴 ALWAYS UPDATE POSTGRESQL FIRST, THEN SHOPIFY!**
When updating ANY product data:
1. **FIRST**: Scrape vendor website for ALL data (colors, specs, images, SKUs)
2. **SECOND**: Save ALL data to PostgreSQL (vendor_catalog or arte_catalog table)
3. **THIRD**: Only THEN update Shopify from PostgreSQL data
**Why PostgreSQL First?**
- PostgreSQL is the source of truth for ALL vendor data
- Prevents data loss if Shopify API fails
- Enables bulk operations and rollback
- Allows verification before pushing to Shopify
- Manufacturer color names MUST come from scraped data, not AI
**Never skip PostgreSQL!** Even for single product updates.
## CRITICAL: BANNED WORD "WALLPAPER" - NEVER USE! #memory
**🚨 The word "WALLPAPER" is BANNED from ALL product titles, descriptions, and AI output.**
| ❌ NEVER Use | ✅ ALWAYS Use |
|-------------|--------------|
| Wallpaper | Wallcovering |
| Wallpapers | Wallcoverings |
| Walls Wallpaper | Wallcoverings |
**Applies to ALL vendors:**
- DW Bespoke Studios (all variants)
- Koroseal
- Maya Romanoff
- Thibaut
- Arte
- All other architectural vendors
**Post-processing cleanup (MANDATORY in all updaters):**
⚠️ MUST be **brand-aware** — 17 legitimate brand names END in "Wallpaper" (Ralph Lauren,
Missoni, Grasscloth, Scalamandre, Schumacher, etc.) and a blanket `\bWallpaper\b` swap
CORRUPTS them (e.g. "Ralph Lauren Wallpaper" → "Ralph Lauren Wallcovering"). It must also
skip the derived words `wallpapered`/`wallpapering`. Use this version (TK-00134):
```javascript
const cleanWallpaper = (str) => {
if (!str) return str;
// Preserve the 17 brands ending in "Wallpaper"; skip wallpapered/wallpapering; case-preserving.
const WALLPAPER_BRANDS = ['Apartment','Boråstapeter','China Seas','DW Exclusive','Edge','Fentucci','Grasscloth','Laura Ashley','Malibu','MC Escher','Missoni','Nicolette Mayer','PS Removable','Ralph Lauren','Roberto Cavalli','Scalamandre','Schumacher'];
const brandRe = new RegExp('(' + WALLPAPER_BRANDS.map(b => b.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|') + ')\\s+Wallpaper', 'gi');
const SENT = '