Data4Autos-Shopify-Backend/Backups/manageProducts copy.js
2026-04-13 05:23:25 +00:00

653 lines
19 KiB
JavaScript
Executable File

// routes/manageBrands.js
const express = require('express');
const axios = require('axios');
const { v4: uuid } = require('uuid');
const { getToken } = require('../tokenStore');
const { log } = require('../logger');
const router = express.Router();
const API_VERSION = '2023-10';
// Simple in-memory process tracker
const processes = {};
function slugify(str) {
return str
.toString()
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
}
const GetAllProductsOfBranch = async (brandId, turn14accessToken) => {
var AllProductsOfBrans = [];
try {
const res = await fetch(`https://turn14.data4autos.com/v1/items/brandallitems/${brandId}`, {
headers: {
Authorization: `Bearer ${turn14accessToken}`,
"Content-Type": "application/json",
},
});
const data = await res.json();
// Ensure we have an array of valid items
const validItems = Array.isArray(data)
? data.filter(item => item && item.id && item.attributes)
: [];
AllProductsOfBrans = validItems
console.log("AllProductsOfBrans", AllProductsOfBrans.length);
const items = Array.isArray(AllProductsOfBrans) ? AllProductsOfBrans.slice(0, 5) : [];
console.log("items", items.length);
return items;
} catch (err) {
console.error("Error fetching items:", err);
return null
}
}
const AddProductToStore = async (shop, accessToken, product) => {
var results = [];
const SHOP = shop
const ACCESS_TOKEN = accessToken
const item = product;
const client = axios.create({
baseURL: `https://${SHOP}/admin/api/${API_VERSION}/graphql.json`,
headers: {
'X-Shopify-Access-Token': ACCESS_TOKEN,
'Content-Type': 'application/json',
},
});
try {
const attrs = item.attributes;
// console.log("Processing item:", attrs);
// Build and normalize collection titles
const category = attrs.category;
const subcategory = attrs.subcategory || "";
const brand = attrs.brand;
const subcats = subcategory
.split(/[,\/]/)
.map((s) => s.trim())
.filter(Boolean);
const collectionTitles = Array.from(
new Set([category, ...subcats, brand].filter(Boolean))
);
// console.log("Collection Titles:", collectionTitles);
// Find or create collections, collect their IDs
const collectionIds = []
for (const title of collectionTitles) {
// console.log(`Searching for collection "${title}"…`);
// 1. Query existing manual collection by title
const lookupQuery = `
query {
collections(first: 1, query: "title:\\"${title}\\" AND collection_type:manual") {
nodes { id }
}
}
`;
const lookupResp = await client.post('', { query: lookupQuery });
const existing = lookupResp.data.data.collections.nodes;
if (existing.length) {
console.log(`→ Found existing collection: ${existing[0].id}`);
collectionIds.push(existing[0].id);
continue;
}
// 2. Otherwise, create it
//console.log(`→ No existing collection. Creating "${title}"…`);
const createMutation = `
mutation collectionCreate($input: CollectionInput!) {
collectionCreate(input: $input) {
collection { id }
userErrors { field message }
}
}
`;
const createResp = await client.post('', {
query: createMutation,
variables: { input: { title } }
});
const createData = createResp.data.data.collectionCreate;
if (createData.userErrors.length) {
throw new Error(
`Could not create collection "${title}": ` +
createData.userErrors.map(e => e.message).join(', ')
);
}
const newId = createData.collection.id;
// console.log(`→ Created collection: ${newId}`);
collectionIds.push(newId);
}
// Build tags
const tags = [
attrs.category,
...subcats,
attrs.brand,
attrs.part_number,
attrs.mfr_part_number,
attrs.price_group,
attrs.units_per_sku && `${attrs.units_per_sku} per SKU`,
attrs.barcode
].filter(Boolean).map((t) => t.trim());
// console.log("Tags:", tags);
// Prepare media inputs
const mediaInputs = (attrs.files || [])
.filter((f) => f.type === "Image" && f.url)
.map((file) => ({
originalSource: file.url,
mediaContentType: "IMAGE",
alt: `${attrs.product_name}${file.media_content}`,
}));
// console.log("Media inputs:", mediaInputs);
// Pick the longest "Market Description" or fallback to part_description
const marketDescs = (attrs.descriptions || [])
.filter((d) => d.type === "Market Description")
.map((d) => d.description);
const descriptionHtml = marketDescs.length
? marketDescs.reduce((a, b) => (b.length > a.length ? b : a))
: attrs.part_description;
// console.log("Description HTML:", descriptionHtml);
// Create product + attach to collections + add media
// const createProdRes = await admin.graphql(`
// mutation($prod: ProductCreateInput!, $media: [CreateMediaInput!]) {
// productCreate(product: $prod, media: $media) {
// product {
// id
// variants(first: 1) {
// nodes { id inventoryItem { id } price compareAtPrice barcode }
// }
// }
// userErrors { field message }
// }
// }
// `, {
// variables: {
// prod: {
// title: attrs.product_name,
// descriptionHtml: descriptionHtml,
// vendor: attrs.brand,
// productType: attrs.category,
// handle: slugify(attrs.part_number || attrs.product_name),
// tags,
// collectionsToJoin: collectionIds,
// status: "ACTIVE",
// },
// media: mediaInputs,
// },
// });
// const createProdRes = await client.post('', {
// query: `
// mutation($prod: ProductCreateInput!, $media: [CreateMediaInput!]) {
// productCreate(product: $prod, media: $media) {
// product {
// id
// variants(first: 1) {
// nodes {
// id
// inventoryItem { id }
// price
// compareAtPrice
// barcode
// }
// }
// }
// userErrors { field message }
// }
// }
// `,
// variables: {
// prod: {
// title: attrs.product_name,
// descriptionHtml: descriptionHtml,
// vendor: attrs.brand,
// productType: attrs.category,
// handle: slugify(item.id + "-" + attrs.mfr_part_number || attrs.product_name),
// tags,
// collectionsToJoin: collectionIds,
// status: "ACTIVE",
// },
// media: mediaInputs,
// },
// });
// const createProdJson = createProdRes.data;
// console.log("Create product response:", createProdJson);
// const prodErrs = createProdJson.data?.productCreate?.userErrors || [];
// if (prodErrs.length) {
// const taken = prodErrs.some(e => /already in use/i.test(e.message));
// if (taken) {
// results.push({ skippedHandle: attrs.part_number, reason: "handle in use" });
// return null
// }
// throw new Error(`ProductCreate errors: ${prodErrs.map(e => e.message).join(", ")}`);
// }
// const product = createProdJson.data.productCreate.product;
const createProdRes = await client.post('', {
query: `
mutation($prod: ProductInput!, $media: [CreateMediaInput!]) {
productCreate(input: $prod, media: $media) {
product {
id
variants(first: 1) {
nodes {
id
inventoryItem { id }
price
compareAtPrice
barcode
}
}
}
userErrors { field message }
}
}
`,
variables: {
prod: {
title: attrs.product_name,
descriptionHtml: descriptionHtml,
vendor: attrs.brand,
productType: attrs.category,
handle: slugify(item.id + "-" + (attrs.mfr_part_number || attrs.product_name)),
tags,
collectionsToJoin: collectionIds,
status: "ACTIVE",
},
media: mediaInputs,
},
});
const createProdJson = createProdRes.data;
//console.log("Create product response:", createProdJson);
const prodErrs = createProdJson.data?.productCreate?.userErrors || [];
if (prodErrs.length) {
const taken = prodErrs.some(e => /already in use/i.test(e.message));
if (taken) {
results.push({ skippedHandle: attrs.part_number, reason: "handle in use" });
return null;
}
throw new Error(`ProductCreate errors: ${prodErrs.map(e => e.message).join(", ")}`);
}
const product = createProdJson.data.productCreate.product;
const variantNode = product.variants?.nodes?.[0];
if (!variantNode) {
console.error("Variant node is undefined for product:", product.id);
return null
}
const variantId = variantNode.id;
const inventoryItemId = variantNode.inventoryItem?.id;
// Bulk-update variant (price, compare-at, barcode)
const price = parseFloat(attrs.price) || 1000;
const comparePrice = parseFloat(attrs.compare_price) || null;
const barcode = attrs.barcode || "";
const bulkRes = await client.post('', {
query: `
mutation($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {
productVariantsBulkUpdate(productId: $productId, variants: $variants) {
productVariants {
id
price
compareAtPrice
barcode
}
userErrors {
field
message
}
}
}
`,
variables: {
productId: product.id,
variants: [{
id: variantId,
price,
...(comparePrice !== null && { compareAtPrice: comparePrice }),
...(barcode && { barcode }),
}],
},
});
const bulkJson = bulkRes.data;
const bulkErrs = bulkJson.data.productVariantsBulkUpdate.userErrors;
if (bulkErrs.length) {
throw new Error(`Bulk update errors: ${bulkErrs.map(e => e.message).join(", ")}`);
}
// Fetch the Online Store publication ID
//console.log("Fetching Online Store publication ID...");
const publicationsRes = await client.post('', {
query: `
query {
publications(first: 10) {
edges {
node {
id
name
}
}
}
}
`
});
const publicationsJson = publicationsRes.data;
// console.log("Publications response:", publicationsJson);
const onlineStorePublication = publicationsJson.data.publications.edges.find(
pub => pub.node.name === 'Online Store'
);
const onlineStorePublicationId = onlineStorePublication ? onlineStorePublication.node.id : null;
if (onlineStorePublicationId) {
// console.log("Publishing product to Online Store...");
// ▶︎ Replace admin.graphql(…) with:
const publishRes = await client.post('', {
query: `
mutation($id: ID!, $publicationId: ID!) {
publishablePublish(id: $id, input: { publicationId: $publicationId }) {
publishable {
... on Product {
id
title
status
}
}
userErrors { field message }
}
}
`,
variables: {
id: product.id,
publicationId: onlineStorePublicationId,
},
});
const publishJson = publishRes.data;
console.log("Publish response:", publishJson);
const publishErrs = publishJson.data.publishablePublish.userErrors;
if (publishErrs.length) {
throw new Error(`Publish errors: ${publishErrs.map(e => e.message).join(", ")}`);
}
} else {
throw new Error("Online Store publication not found.");
}
// Update inventory item (SKU, cost & weight)
const costPerItem = parseFloat(attrs.purchase_cost) || 0;
const weightValue = parseFloat(attrs.dimensions?.[0]?.weight) || 0;
// console.log("Updating inventory item...");
// const invRes = await client.post('', {
// query: `
// mutation($id: ID!, $input: InventoryItemInput!) {
// inventoryItemUpdate(id: $id, input: $input) {
// inventoryItem {
// id
// sku
// measurement {
// weight { value unit }
// }
// }
// userErrors { field message }
// }
// }
// `,
// variables: {
// id: inventoryItemId,
// input: {
// sku: attrs.part_number,
// cost: parseFloat(attrs.purchase_cost) || 0,
// measurement: {
// weight: {
// value: parseFloat(attrs.dimensions?.[0]?.weight) || 0,
// unit: "POUNDS"
// }
// }
// }
// }
// });
// const invJson = invRes.data;
// console.log("Inventory update response:", invJson);
// const invErrs = invJson.data.inventoryItemUpdate.userErrors;
// if (invErrs.length) {
// throw new Error(`Inventory update errors: ${invErrs.map(e => e.message).join(", ")}`);
// }
// const inventoryItem = invJson.data.inventoryItemUpdate.inventoryItem;
// console.log("Updating inventory item...");
// const invRes = await client.post('', {
// query: `
// mutation($id: ID!, $input: InventoryItemUpdateInput!) {
// inventoryItemUpdate(id: $id, input: $input) {
// inventoryItem {
// id
// sku
// measurement {
// weight { value unit }
// }
// }
// userErrors { field message }
// }
// }
// `,
// variables: {
// id: inventoryItemId,
// input: {
// sku: attrs.part_number,
// cost: parseFloat(attrs.purchase_cost) || 0,
// measurement: {
// weight: {
// value: parseFloat(attrs.dimensions?.[0]?.weight) || 0,
// unit: "POUNDS"
// }
// }
// }
// }
// });
// const invJson = invRes.data;
// console.log("Inventory update response:", invJson);
// const invErrs = invJson.data.inventoryItemUpdate.userErrors;
// if (invErrs.length) {
// throw new Error(`Inventory update errors: ${invErrs.map(e => e.message).join(", ")}`);
// }
// const inventoryItem = invJson.data.inventoryItemUpdate.inventoryItem;
console.log("Updating inventory item...");
const invRes = await client.post('', {
query: `
mutation($id: ID!, $input: InventoryItemUpdateInput!) {
inventoryItemUpdate(id: $id, input: $input) {
inventoryItem {
id
unitCost { amount }
tracked
}
userErrors { field message }
}
}
`,
variables: {
id: inventoryItemId,
input: {
cost: parseFloat(attrs.purchase_cost) || 0,
tracked: true
}
}
});
const invJson = invRes.data;
console.log("Inventory update response:", invJson);
const invErrs = invJson.data.inventoryItemUpdate.userErrors;
if (invErrs.length) {
throw new Error(`Inventory update errors: ${invErrs.map(e => e.message).join(", ")}`);
}
const inventoryItem = invJson.data.inventoryItemUpdate.inventoryItem;
// Collect results
results.push({
productId: product.id,
variant: {
id: variantId,
price: variantNode.price,
compareAtPrice: variantNode.compareAtPrice,
sku: inventoryItem.sku,
barcode: variantNode.barcode,
weight: inventoryItem?.measurement?.weight.value,
weightUnit: inventoryItem?.measurement?.weight.unit,
},
collections: collectionTitles,
tags,
});
return results;
} catch (err) {
console.error("Error in AddProductToStore:", err);
results.push({ error: `Failed to process item ${item.id}: ${err.message}` });
return results;
}
}
const GetAllProductsandAddToStore = async (shop, accessToken, brandId, turn14accessToken) => {
const products = await GetAllProductsOfBranch(brandId, turn14accessToken);
if (!products) {
console.error("No products found or error fetching products.");
return [];
}
const results = [];
for (const item of products) {
const res = await AddProductToStore(shop, accessToken, item);
results.push(...res);
}
return results;
}
async function fetchAllCollections(shop, accessToken) {
const adminUrl = `https://${shop}/admin/api/2023-10/graphql.json`;
const headers = {
'X-Shopify-Access-Token': accessToken,
'Content-Type': 'application/json',
};
let allCollections = [];
let hasNextPage = true;
let endCursor = null;
const pageSize = 100;
while (hasNextPage) {
const fetchQuery = `
query GetCollections {
collections(first: ${pageSize}${endCursor ? `, after: "${endCursor}"` : ''}) {
edges {
node {
id
title
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}
`;
const fetchResp = await axios.post(adminUrl, { query: fetchQuery }, { headers });
const collections = fetchResp.data.data.collections.edges.map(e => e.node);
allCollections = allCollections.concat(collections);
hasNextPage = fetchResp.data.data.collections.pageInfo.hasNextPage;
endCursor = fetchResp.data.data.collections.pageInfo.endCursor;
}
return allCollections;
}
router.post('/', async (req, res) => {
const { shop, brandID, turn14accessToken } = req.body;
const procId = uuid();
processes[procId] = { status: 'started', detail: null };
res.json({ processId: procId, status: 'started' });
(async () => {
try {
log(shop, `🔔 [${procId}] ManageBrands started`);
processes[procId].status = 'fetching_collections';
// 1. Get token
const tokenRecord = getToken(shop);
if (!tokenRecord) throw new Error('No token for shop');
const allCollections = await GetAllProductsandAddToStore(shop, tokenRecord.accessToken, brandID, turn14accessToken);
log(shop, `🔍 [${procId}] Fetchedd ${allCollections.length} existing collections`);
} catch (err) {
processes[procId].status = 'error';
processes[procId].detail = err.message;
log(shop, `❌ [${procId}] Error: ${err.message}`);
}
})();
});
router.get('/status/:processId', (req, res) => {
const info = processes[req.params.processId];
if (!info) return res.status(404).json({ error: 'Not found' });
res.json(info);
});
module.exports = router;