{"name":"AliExpress to store workflow","nodes":[{"id":"a1b2c301-0001-4001-8001-998877670001","name":"Daily Import Run","type":"n8n-nodes-base.scheduleTrigger","typeVersion":1.2,"position":[-260,300],"parameters":{"rule":{"interval":[{"field":"hours","hoursInterval":24}]}}},{"id":"a1b2c302-0002-4002-8002-998877680002","name":"Read Supplier Queue","type":"n8n-nodes-base.googleSheets","typeVersion":4.5,"position":[0,300],"parameters":{"operation":"read","documentId":{"__rl":true,"value":"SHEET_ID","mode":"id"},"sheetName":{"__rl":true,"value":"gid=0","mode":"list","cachedResultName":"Supplier Queue"},"options":{}}},{"id":"a1b2c303-0003-4003-8003-998877690003","name":"Dedupe And Pace Queue","type":"n8n-nodes-base.code","typeVersion":2,"position":[260,300],"parameters":{"jsCode":"// Normalise the supplier queue sheet and drop anything already in the store.\n// Sheet columns: supplier_url, niche, imported_sku (blank if not yet imported)\nconst rows = $input.all().map(i => i.json);\nconst seen = new Set();\nconst queue = [];\n\nfor (const r of rows) {\n  const url = String(r.supplier_url || '').trim();\n  if (!url) continue;\n  if (String(r.imported_sku || '').trim()) continue;   // already live in store\n  // AliExpress ids look like .../item/1005006123456789.html\n  const m = url.match(/(?:item\\/|\\/i\\/)(\\d{10,})/);\n  if (!m) continue;\n  const id = m[1];\n  if (seen.has(id)) continue;                           // dedupe within the sheet\n  seen.add(id);\n  queue.push({ json: {\n    product_id: id,\n    supplier_url: url,\n    niche: String(r.niche || 'general').toLowerCase().trim(),\n    row_number: r.row_number ?? null,\n  }});\n}\n\n// pace the run: never import more than 25 products a day\nreturn queue.slice(0, 25);"}},{"id":"a1b2c304-0004-4004-8004-9988776a0004","name":"Fetch AliExpress Product","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[520,300],"parameters":{"url":"https://api-sg.aliexpress.com/sync","sendQuery":true,"queryParameters":{"parameters":[{"name":"method","value":"aliexpress.ds.product.get"},{"name":"product_id","value":"={{ $json.product_id }}"},{"name":"ship_to_country","value":"US"},{"name":"target_currency","value":"USD"},{"name":"target_language","value":"en"}]},"options":{}}},{"id":"a1b2c305-0005-4005-8005-9988776b0005","name":"Price And Score Product","type":"n8n-nodes-base.code","typeVersion":2,"position":[780,300],"parameters":{"jsCode":"// ---- Pricing engine + import scoring -------------------------------------\n// Input: one AliExpress product detail payload per item.\n// Store economics (edit per store):\nconst FX = 1.00;                 // supplier currency -> store currency\nconst PAYMENT_FEE_PCT = 0.029;   // Shopify Payments\nconst PAYMENT_FEE_FLAT = 0.30;\nconst AD_COST_PCT = 0.25;        // blended paid-traffic cost as % of retail\nconst RETURN_RESERVE_PCT = 0.03;\nconst MIN_MARGIN_PCT = 0.35;     // margin FLOOR after fees + ads\nconst MIN_MARGIN_ABS = 8.00;     // absolute profit floor per unit\nconst TARGET_MULTIPLE = 3.2;     // desired landed-cost multiple\nconst PRICE_CEILING = 149.00;    // above this impulse buying dies\n\nfunction psychological(p) {\n  // round up to the nearest .95 ending, never round down below the floor\n  const base = Math.floor(p);\n  const cand = base + 0.95;\n  return +(cand >= p ? cand : base + 1.95).toFixed(2);\n}\n\n// the HTTP node drops the queue fields, so re-attach them by paired index\nconst queue = $('Dedupe And Pace Queue').all();\n\nconst out = [];\nconst items = $input.all();\nfor (let i = 0; i < items.length; i++) {\n  const item = items[i];\n  const p = item.json.result || item.json;\n  const src = (queue[item.pairedItem?.item ?? i] || queue[i] || {}).json || {};\n\n  const sku          = String(p.product_id ?? p.productId ?? '');\n  const title        = String(p.product_title ?? p.subject ?? '').trim();\n  const supplierCost = Number(p.target_sale_price ?? p.sale_price ?? 0) * FX;\n  const shippingCost = Number(p.ship_to_price ?? p.logistics?.shipping_fee ?? 0) * FX;\n  const origPrice    = Number(p.target_original_price ?? p.original_price ?? supplierCost);\n  const orders       = Number(p.lastest_volume ?? p.orders_count ?? 0);\n  const rating       = Number(p.evaluate_rate ?? p.average_star ?? 0);\n  const reviews      = Number(p.evaluation_count ?? 0);\n  const shipDays     = Number(p.delivery_time ?? p.logistics?.delivery_days ?? 25);\n  const images       = Array.isArray(p.product_small_image_urls) ? p.product_small_image_urls : (p.image_urls || []);\n\n  const landed = +(supplierCost + shippingCost).toFixed(2);\n  if (!sku || landed <= 0) continue;\n\n  // 1) target price from the multiple, clamped by the ceiling\n  let price = psychological(Math.min(landed * TARGET_MULTIPLE, PRICE_CEILING));\n\n  // 2) walk the price up until BOTH margin floors clear\n  const profitAt = (rp) => rp\n    - landed\n    - (rp * PAYMENT_FEE_PCT + PAYMENT_FEE_FLAT)\n    - (rp * AD_COST_PCT)\n    - (rp * RETURN_RESERVE_PCT);\n\n  let guard = 0;\n  while (guard++ < 60) {\n    const profit = profitAt(price);\n    const pct = profit / price;\n    if (pct >= MIN_MARGIN_PCT && profit >= MIN_MARGIN_ABS) break;\n    price = psychological(price + 1);\n  }\n\n  const profit = +profitAt(price).toFixed(2);\n  const marginPct = +(profit / price).toFixed(4);\n  const compareAt = psychological(price * 1.45);          // anchor price\n  const floorsCleared = marginPct >= MIN_MARGIN_PCT && profit >= MIN_MARGIN_ABS && price <= PRICE_CEILING;\n\n  // 3) import score 0-100: demand, trust, logistics, discount depth, headroom\n  const demand   = Math.min(orders / 1500, 1) * 30;\n  const trust    = (Math.max(rating - 4.2, 0) / 0.8) * 20 + Math.min(reviews / 400, 1) * 10;\n  const logistic = shipDays <= 10 ? 20 : shipDays <= 15 ? 14 : shipDays <= 22 ? 8 : 2;\n  const discount = origPrice > supplierCost\n    ? Math.min((origPrice - supplierCost) / origPrice, 0.5) * 10 : 0;\n  const headroom = Math.min(Math.max(marginPct - MIN_MARGIN_PCT, 0) / 0.25, 1) * 10;\n  const score = Math.round(demand + trust + logistic + discount + headroom);\n\n  const reasons = [];\n  if (!floorsCleared) reasons.push('margin floor unreachable under price ceiling');\n  if (orders < 300) reasons.push('low order volume (' + orders + ')');\n  if (rating && rating < 4.5) reasons.push('rating ' + rating);\n  if (shipDays > 25) reasons.push('ship time ' + shipDays + 'd');\n  if (images.length < 3) reasons.push('only ' + images.length + ' images');\n\n  out.push({ json: {\n    sku, supplier_title: title,\n    supplier_url: p.product_detail_url || src.supplier_url || '',\n    niche: src.niche || 'general',\n    supplier_cost: +supplierCost.toFixed(2), shipping_cost: +shippingCost.toFixed(2),\n    landed_cost: landed, price, compare_at_price: compareAt,\n    unit_profit: profit, margin_pct: marginPct,\n    orders, rating, reviews, ship_days: shipDays,\n    images: images.slice(0, 6),\n    import_score: score,\n    floors_cleared: floorsCleared,\n    reject_reasons: reasons.join('; ') || 'none',\n    priced_at: new Date().toISOString(),\n  }});\n}\n\n// best products first so a partial run still imports the winners\nout.sort((a, b) => b.json.import_score - a.json.import_score);\nreturn out;"}},{"id":"a1b2c306-0006-4006-8006-9988776c0006","name":"Margin Floors Cleared?","type":"n8n-nodes-base.if","typeVersion":2,"position":[1040,300],"parameters":{"conditions":{"options":{"caseSensitive":true,"version":2},"combinator":"and","conditions":[{"id":"c1","leftValue":"={{ $json.floors_cleared }}","rightValue":"","operator":{"type":"boolean","operation":"true","singleValue":true}},{"id":"c2","leftValue":"={{ $json.import_score }}","rightValue":55,"operator":{"type":"number","operation":"gte"}}]},"options":{}}},{"id":"a1b2c307-0007-4007-8007-9988776d0007","name":"Rewrite Title And Description","type":"@n8n/n8n-nodes-langchain.openAi","typeVersion":1.8,"position":[1300,160],"parameters":{"modelId":{"__rl":true,"value":"gpt-4o-mini","mode":"list"},"messages":{"values":[{"role":"system","content":"You are a DTC ecommerce copywriter. Rewrite scraped AliExpress listings into clean western-store copy. Never mention AliExpress, China, dropshipping or shipping times. Reply with ONLY valid JSON: {\"title\": string (max 65 chars, no keyword stuffing), \"description\": string (2-3 sentences, benefit-first), \"bullets\": string[] (4 short benefit bullets)}."},{"content":"=Niche: {{ $json.niche }}\nSupplier title: {{ $json.supplier_title }}\nRetail price: ${{ $json.price }}\nOrders: {{ $json.orders }} | Rating: {{ $json.rating }}\nRewrite this listing."}]},"options":{}}},{"id":"a1b2c308-0008-4008-8008-9988776e0008","name":"Build Listing Payload","type":"n8n-nodes-base.code","typeVersion":2,"position":[1560,160],"parameters":{"jsCode":"// Parse the copywriter JSON, sanity-check it, and build the Shopify payload.\nconst out = [];\nfor (const item of $input.all()) {\n  const priced = $('Price And Score Product').all()[item.pairedItem?.item ?? 0]?.json\n    || $('Price And Score Product').first().json;\n\n  let copy = {};\n  const raw = item.json.message?.content ?? item.json.content ?? item.json.text ?? '{}';\n  try { copy = typeof raw === 'string' ? JSON.parse(raw.replace(/```json|```/g, '').trim()) : raw; }\n  catch (e) { copy = {}; }\n\n  // fall back to a cleaned supplier title if the LLM misbehaved\n  const cleanFallback = priced.supplier_title\n    .replace(/\\b(free shipping|hot sale|dropship(ping)?|wholesale|new arrival|\\d+pcs?)\\b/gi, '')\n    .replace(/[|,\\-–]+\\s*$/,'').replace(/\\s{2,}/g,' ').trim();\n\n  let title = String(copy.title || cleanFallback).trim().slice(0, 70);\n  if (title.length < 12) title = cleanFallback.slice(0, 70);\n\n  const bullets = Array.isArray(copy.bullets) ? copy.bullets.slice(0, 5) : [];\n  const body = '<p>' + String(copy.description || cleanFallback) + '</p>'\n    + (bullets.length ? '<ul>' + bullets.map(b => '<li>' + b + '</li>').join('') + '</ul>' : '');\n\n  const handle = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n\n  out.push({ json: {\n    ...priced,\n    listing_title: title,\n    listing_body_html: body,\n    listing_handle: handle,\n    seo_title: title.slice(0, 60),\n    tags: [priced.niche || 'import', 'aliexpress', 'score-' + priced.import_score].join(','),\n    product_payload: {\n      title,\n      body_html: body,\n      vendor: 'AliExpress Supplier',\n      handle,\n      status: 'draft',\n      tags: (priced.niche || 'import') + ',aliexpress-import',\n      images: (priced.images || []).map(src => ({ src })),\n      variants: [{\n        sku: 'AE-' + priced.sku,\n        price: priced.price.toFixed(2),\n        compare_at_price: priced.compare_at_price.toFixed(2),\n        inventory_management: null,\n        requires_shipping: true,\n      }],\n    },\n  }});\n}\nreturn out;"}},{"id":"a1b2c309-0009-4009-8009-9988776f0009","name":"Create Store Listing","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1820,160],"parameters":{"method":"POST","url":"https://STORE_NAME.myshopify.com/admin/api/2024-10/products.json","sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify({ product: $json.product_payload }) }}","options":{}}},{"id":"a1b2c30a-000a-400a-800a-99887770000a","name":"Shape Import Log Row","type":"n8n-nodes-base.set","typeVersion":3.3,"position":[2080,160],"parameters":{"assignments":{"assignments":[{"id":"a1","name":"sku","value":"={{ $('Build Listing Payload').item.json.sku }}","type":"string"},{"id":"a2","name":"title","value":"={{ $('Build Listing Payload').item.json.listing_title }}","type":"string"},{"id":"a3","name":"shopify_product_id","value":"={{ $json.product?.id }}","type":"string"},{"id":"a4","name":"landed_cost","value":"={{ $('Build Listing Payload').item.json.landed_cost }}","type":"number"},{"id":"a5","name":"price","value":"={{ $('Build Listing Payload').item.json.price }}","type":"number"},{"id":"a6","name":"unit_profit","value":"={{ $('Build Listing Payload').item.json.unit_profit }}","type":"number"},{"id":"a7","name":"margin_pct","value":"={{ $('Build Listing Payload').item.json.margin_pct }}","type":"number"},{"id":"a8","name":"import_score","value":"={{ $('Build Listing Payload').item.json.import_score }}","type":"number"},{"id":"a9","name":"status","value":"imported_draft","type":"string"},{"id":"a10","name":"imported_at","value":"={{ $now.toISO() }}","type":"string"}]},"options":{}}},{"id":"a1b2c30b-000b-400b-800b-99887771000b","name":"Append Import Log","type":"n8n-nodes-base.googleSheets","typeVersion":4.5,"position":[2340,160],"parameters":{"operation":"append","documentId":{"__rl":true,"value":"SHEET_ID","mode":"id"},"sheetName":{"__rl":true,"value":"gid=111111","mode":"list","cachedResultName":"Import Log"},"columns":{"mappingMode":"autoMapInputData","value":{},"matchingColumns":[]},"options":{}}},{"id":"a1b2c30c-000c-400c-800c-99887772000c","name":"Notify Buyer Of New Listings","type":"n8n-nodes-base.slack","typeVersion":2.2,"position":[2600,160],"parameters":{"select":"channel","channelId":{"__rl":true,"value":"#store-imports","mode":"name"},"text":"=🆕 Imported *{{ $json.title }}* (SKU {{ $json.sku }}) — ${{ $json.price }} retail, ${{ $json.unit_profit }} profit ({{ Math.round($json.margin_pct * 100) }}% margin), score {{ $json.import_score }}. Draft is waiting for review.","otherOptions":{}}},{"id":"a1b2c30d-000d-400d-800d-99887773000d","name":"Shape Rejection Row","type":"n8n-nodes-base.set","typeVersion":3.3,"position":[1300,520],"parameters":{"assignments":{"assignments":[{"id":"r1","name":"sku","value":"={{ $json.sku }}","type":"string"},{"id":"r2","name":"supplier_title","value":"={{ $json.supplier_title }}","type":"string"},{"id":"r3","name":"supplier_url","value":"={{ $json.supplier_url }}","type":"string"},{"id":"r4","name":"landed_cost","value":"={{ $json.landed_cost }}","type":"number"},{"id":"r5","name":"best_price_tested","value":"={{ $json.price }}","type":"number"},{"id":"r6","name":"margin_pct","value":"={{ $json.margin_pct }}","type":"number"},{"id":"r7","name":"import_score","value":"={{ $json.import_score }}","type":"number"},{"id":"r8","name":"status","value":"rejected","type":"string"},{"id":"r9","name":"reject_reasons","value":"={{ $json.reject_reasons }}","type":"string"},{"id":"r10","name":"checked_at","value":"={{ $now.toISO() }}","type":"string"}]},"options":{}}},{"id":"a1b2c30e-000e-400e-800e-99887774000e","name":"Append Rejected Products","type":"n8n-nodes-base.googleSheets","typeVersion":4.5,"position":[1560,520],"parameters":{"operation":"append","documentId":{"__rl":true,"value":"SHEET_ID","mode":"id"},"sheetName":{"__rl":true,"value":"gid=222222","mode":"list","cachedResultName":"Rejected"},"columns":{"mappingMode":"autoMapInputData","value":{},"matchingColumns":[]},"options":{}}},{"id":"a1b2c30f-000f-400f-800f-99887775000f","name":"Log Rejection To Warehouse","type":"n8n-nodes-base.postgres","typeVersion":2.5,"position":[1820,520],"parameters":{"operation":"executeQuery","query":"insert into supplier_import_rejects (sku, landed_cost, margin_pct, import_score, reasons, checked_on) values ($1,$2,$3,$4,$5, current_date)","options":{"queryReplacement":"={{ $json.sku }},{{ $json.landed_cost }},{{ $json.margin_pct }},{{ $json.import_score }},{{ $json.reject_reasons }}"}}},{"id":"a1b2c310-0010-4010-8010-998877760010","name":"Section: Source And Dedupe","type":"n8n-nodes-base.stickyNote","typeVersion":1,"position":[-300,60],"parameters":{"content":"## 1. SOURCE + DEDUPE\nPull the supplier queue sheet, drop rows already imported, extract the AliExpress product id from the URL, dedupe, and cap the run at 25 products/day.","height":200,"width":460,"color":4}},{"id":"a1b2c311-0011-4011-8011-998877770011","name":"Section: Pricing Engine","type":"n8n-nodes-base.stickyNote","typeVersion":1,"position":[760,60],"parameters":{"content":"## 2. PRICING + SCORING\nLanded cost = item + shipping. Price starts at 3.2x, then walks up in $1 steps until it clears BOTH floors: 35% margin after payment fees, 25% ad cost and 3% returns, AND $8 absolute profit — capped at a $149 impulse ceiling. Score 0-100 = demand + trust + ship speed + discount + margin headroom.","height":260,"width":500,"color":3}},{"id":"a1b2c312-0012-4012-8012-998877780012","name":"Section: Publish Or Reject","type":"n8n-nodes-base.stickyNote","typeVersion":1,"position":[1560,-140],"parameters":{"content":"## 3. PUBLISH OR REJECT\nWinners get LLM-rewritten copy and a Shopify draft product + log row + Slack ping. Losers are written to the Rejected tab and the warehouse so the same dud is never re-sourced.","height":200,"width":460,"color":5}}],"connections":{"Daily Import Run":{"main":[[{"node":"Read Supplier Queue","type":"main","index":0}]]},"Read Supplier Queue":{"main":[[{"node":"Dedupe And Pace Queue","type":"main","index":0}]]},"Dedupe And Pace Queue":{"main":[[{"node":"Fetch AliExpress Product","type":"main","index":0}]]},"Fetch AliExpress Product":{"main":[[{"node":"Price And Score Product","type":"main","index":0}]]},"Price And Score Product":{"main":[[{"node":"Margin Floors Cleared?","type":"main","index":0}]]},"Margin Floors Cleared?":{"main":[[{"node":"Rewrite Title And Description","type":"main","index":0}],[{"node":"Shape Rejection Row","type":"main","index":0}]]},"Rewrite Title And Description":{"main":[[{"node":"Build Listing Payload","type":"main","index":0}]]},"Build Listing Payload":{"main":[[{"node":"Create Store Listing","type":"main","index":0}]]},"Create Store Listing":{"main":[[{"node":"Shape Import Log Row","type":"main","index":0}]]},"Shape Import Log Row":{"main":[[{"node":"Append Import Log","type":"main","index":0}]]},"Append Import Log":{"main":[[{"node":"Notify Buyer Of New Listings","type":"main","index":0}]]},"Shape Rejection Row":{"main":[[{"node":"Append Rejected Products","type":"main","index":0}]]},"Append Rejected Products":{"main":[[{"node":"Log Rejection To Warehouse","type":"main","index":0}]]}},"settings":{"executionOrder":"v1"},"pinData":{}}