← back to Designer Wallcoverings

DW-Agents/dw-agents/koroseal-fixed-import.ts

314 lines

/**
 * FIXED Koroseal Import - Images, Specs, Tags
 * Removed invalid 'variants' field from ProductInput
 */

import { chromium } from 'playwright';
import Anthropic from '@anthropic-ai/sdk';

const SHOPIFY_STORE = 'designer-laboratory-sandbox.myshopify.com';
const SHOPIFY_TOKEN = (process.env.SHOPIFY_ADMIN_TOKEN || '');
const anthropic = new Anthropic();

interface Product {
  sku: string;
  color: string;
  collection: string;
  collectionUrl: string;
  image?: string;
  specs?: any;
  tags?: string[];
}

// Collections to import with their URLs
const COLLECTIONS = [
  { name: 'adalyn-weave', url: 'https://koroseal.com/products/wallcoverings/adalyn-weave', type: 'wallcovering' },
  { name: 'chateau', url: 'https://koroseal.com/products/wallcoverings/chateau', type: 'wallcovering' },
  { name: 'spire', url: 'https://koroseal.com/products/wallcoverings/spire', type: 'wallcovering' },
  { name: 'muga-silk', url: 'https://koroseal.com/products/wallcoverings/muga-silk', type: 'wallcovering' },
  { name: 'color-wash-cork', url: 'https://koroseal.com/products/specialty/koroseal-specialty/color-wash-cork', type: 'specialty' },
  { name: 'woven-wood', url: 'https://koroseal.com/products/specialty/koroseal-specialty/woven-wood', type: 'specialty' },
  { name: 'quercus', url: 'https://koroseal.com/products/specialty/koroseal-specialty/quercus', type: 'specialty' },
  { name: 'kilika', url: 'https://koroseal.com/products/specialty/koroseal-specialty/kilika', type: 'specialty' },
  { name: 'ruche', url: 'https://koroseal.com/products/specialty/koroseal-specialty/ruche', type: 'specialty' },
];

function generateDWWC(): string {
  const ts = Date.now().toString().slice(-10);
  const rand = Math.floor(Math.random() * 10000).toString().padStart(4, '0');
  return `DWWC-KOR-${ts}-${rand}`;
}

async function scrapeCollection(collectionUrl: string): Promise<{ products: Product[], specs: any }> {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();
  const products: Product[] = [];
  let specs: any = {};

  try {
    await page.goto(collectionUrl, { waitUntil: 'load', timeout: 45000 });
    await page.waitForTimeout(4000);

    // Get colorways with images
    const colorways = await page.evaluate(() => {
      const results: any[] = [];
      document.querySelectorAll('.thumbscontain a.thumbprev').forEach((el) => {
        const link = el as HTMLAnchorElement;
        const skuEl = el.querySelector('span.tptext');
        const skuText = skuEl?.childNodes[0]?.textContent?.trim() || '';
        const colorEl = el.querySelector('span.tptext em');
        const colorName = colorEl?.textContent?.trim() || '';
        const picEl = el.querySelector('span.tppic') as HTMLElement;
        const bgImage = picEl?.style.backgroundImage || '';
        const imageMatch = bgImage.match(/url\(['"]?([^'"]+)['"]?\)/);
        const image = imageMatch ? imageMatch[1] : '';

        if (skuText) {
          results.push({
            sku: skuText.toUpperCase(),
            color: colorName,
            image: image
          });
        }
      });
      return results;
    });

    // Get specs from page
    specs = await page.evaluate(() => {
      const allText = document.body.innerText;
      const result: any = { pdfs: [] };

      const patterns: [string, RegExp][] = [
        ['material', /Material\s*:\s*([^\n]+)/i],
        ['backing', /Backing\s*:\s*([^\n]+)/i],
        ['fireRating', /Fire Rating\s*:\s*([^\n]+)/i],
        ['rollWidth', /Roll Width[^:]*:\s*([^\n]+)/i],
        ['patternMatch', /Pattern Match\s*:\s*([^\n]+)/i],
        ['verticalRepeat', /Vertical Repeat\s*:\s*([^\n]+)/i],
      ];

      for (const [key, regex] of patterns) {
        const match = allText.match(regex);
        if (match) result[key] = match[1].trim();
      }

      // Get PDFs
      document.querySelectorAll('a[href*=".pdf"]').forEach((el) => {
        const link = el as HTMLAnchorElement;
        const name = el.textContent?.trim() || 'Document';
        if (!result.pdfs.some((p: any) => p.url === link.href)) {
          result.pdfs.push({ name, url: link.href });
        }
      });

      return result;
    });

    const collectionName = collectionUrl.split('/').pop() || '';
    for (const c of colorways) {
      products.push({
        ...c,
        collection: collectionName,
        collectionUrl
      });
    }

  } catch (error) {
    console.log(`  Error: ${(error as Error).message}`);
  }

  await browser.close();
  return { products, specs };
}

async function analyzeImageForTags(imageUrl: string): Promise<string[]> {
  try {
    const imageResponse = await fetch(imageUrl);
    const imageBuffer = await imageResponse.arrayBuffer();
    const base64Image = Buffer.from(imageBuffer).toString('base64');

    const response = await anthropic.messages.create({
      model: 'claude-opus-4-8',
      max_tokens: 500,
      messages: [{
        role: 'user',
        content: [
          {
            type: 'image',
            source: { type: 'base64', media_type: 'image/jpeg', data: base64Image }
          },
          {
            type: 'text',
            text: `Analyze this wallcovering image and return ONLY a comma-separated list of interior design tags. Include:
- Background color (e.g., "Background Color Teal")
- 2-3 color names
- Style (Contemporary, Traditional, etc.)
- Pattern type (Textured, Woven, etc.)
- Material look (Silk-Look, Natural Fiber, etc.)
Return ONLY the comma-separated tags, nothing else.`
          }
        ]
      }]
    });

    const text = response.content[0];
    if (text && 'text' in text) {
      return text.text.split(',').map(t => t.trim()).filter(Boolean);
    }
  } catch (error) {
    console.log(`    Tag error: ${(error as Error).message}`);
  }
  return [];
}

async function createProduct(product: Product, specs: any, collectionType: string): Promise<boolean> {
  const dwwc = generateDWWC();
  const collectionName = product.collection.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
  const title = `${product.color} ${collectionName} Wallcovering (${product.sku})`;
  const productType = collectionType === 'specialty' ? 'Specialty Wallcovering' : 'Type II Wallcovering';

  // Build description with specs
  let description = `<h2>${collectionName} Collection</h2>
<p><strong>SKU:</strong> ${product.sku}</p>
<p><strong>DWWC:</strong> ${dwwc}</p>
<p><strong>Color:</strong> ${product.color}</p>
<p><strong>Vendor:</strong> Koroseal</p>`;

  if (specs && Object.keys(specs).length > 0) {
    description += `<h3>Specifications</h3><ul>`;
    if (specs.material) description += `<li><strong>Material:</strong> ${specs.material}</li>`;
    if (specs.backing) description += `<li><strong>Backing:</strong> ${specs.backing}</li>`;
    if (specs.fireRating) description += `<li><strong>Fire Rating:</strong> ${specs.fireRating}</li>`;
    if (specs.rollWidth) description += `<li><strong>Roll Width:</strong> ${specs.rollWidth}</li>`;
    if (specs.patternMatch) description += `<li><strong>Pattern Match:</strong> ${specs.patternMatch}</li>`;
    if (specs.verticalRepeat) description += `<li><strong>Vertical Repeat:</strong> ${specs.verticalRepeat}</li>`;
    description += `</ul>`;

    if (specs.pdfs && specs.pdfs.length > 0) {
      description += `<h3>Documentation</h3><ul>`;
      specs.pdfs.slice(0, 5).forEach((pdf: any) => {
        description += `<li><a href="${pdf.url}" target="_blank">${pdf.name}</a></li>`;
      });
      description += `</ul>`;
    }
  }

  // Build tags
  const baseTags = [
    'Koroseal',
    collectionName,
    product.color,
    product.sku,
    dwwc,
    productType,
    'Commercial Grade'
  ];

  // Get AI-generated tags if image available
  let aiTags: string[] = [];
  if (product.image) {
    aiTags = await analyzeImageForTags(product.image);
  }

  const allTags = [...new Set([...baseTags, ...aiTags])];

  // FIXED: Removed variants from ProductInput - not valid in Shopify GraphQL
  const mutation = `
    mutation productCreate($input: ProductInput!, $media: [CreateMediaInput!]) {
      productCreate(input: $input, media: $media) {
        product { id title }
        userErrors { field message }
      }
    }
  `;

  const variables: any = {
    input: {
      title,
      descriptionHtml: description,
      vendor: 'Koroseal',
      productType,
      tags: allTags,
      status: 'DRAFT'
    }
  };

  // Add image if available
  if (product.image) {
    variables.media = [{
      originalSource: product.image,
      alt: title,
      mediaContentType: 'IMAGE'
    }];
  }

  try {
    const response = await fetch(`https://${SHOPIFY_STORE}/admin/api/2024-01/graphql.json`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Shopify-Access-Token': SHOPIFY_TOKEN,
      },
      body: JSON.stringify({ query: mutation, variables }),
    });

    const data = await response.json();

    if (data.data?.productCreate?.product?.id) {
      const id = data.data.productCreate.product.id.split('/').pop();
      console.log(`    ✓ Created ${id} with ${aiTags.length} AI tags`);
      return true;
    }

    if (data.data?.productCreate?.userErrors?.length > 0) {
      console.log(`    ✗ ${data.data.productCreate.userErrors[0].message}`);
    } else if (data.errors) {
      console.log(`    ✗ ${data.errors[0]?.message || 'API Error'}`);
    }
    return false;
  } catch (error) {
    console.log(`    ✗ ${(error as Error).message}`);
    return false;
  }
}

async function main() {
  console.log('=== KOROSEAL IMPORT (FIXED) ===');
  console.log('Images + Specs + Interior Designer Tags + DWWC Numbers\n');

  let totalCreated = 0;
  let totalFailed = 0;

  for (const collection of COLLECTIONS) {
    console.log(`\n📁 ${collection.name.toUpperCase()}`);
    console.log(`   Scraping ${collection.url}...`);

    const { products, specs } = await scrapeCollection(collection.url);
    console.log(`   Found ${products.length} colorways`);

    if (specs.material) {
      console.log(`   Specs: ${specs.material}, ${specs.backing || 'N/A'}`);
    }

    for (let i = 0; i < products.length; i++) {
      const product = products[i];
      console.log(`   [${i + 1}/${products.length}] ${product.sku} - ${product.color}`);
      console.log(`      Image: ${product.image ? '✓' : '✗'}`);

      const success = await createProduct(product, specs, collection.type);
      if (success) totalCreated++;
      else totalFailed++;

      // Rate limiting
      await new Promise(r => setTimeout(r, 1000));
    }
  }

  console.log('\n=== IMPORT COMPLETE ===');
  console.log(`Created: ${totalCreated}`);
  console.log(`Failed: ${totalFailed}`);
}

main().catch(console.error);