Data4Autos-Shopify-Backend/routes/manageProductBak.js
2026-04-13 05:23:25 +00:00

790 lines
22 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 = '2024-01';
// 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, shop, procId) => {
var AllProductsOfBrans = [];
try {
log(shop, `🔍 [${procId}] Fetching products for brand ${brandId}`);
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;
log(shop, `📦 [${procId}] Found ${AllProductsOfBrans.length} products for brand ${brandId}`);
const items = Array.isArray(AllProductsOfBrans) ? AllProductsOfBrans.slice(0, 3) : [];
//const items = Array.isArray(AllProductsOfBrans) ? AllProductsOfBrans : [];
log(shop, `📝 [${procId}] Processing ${items.length} sample products`);
return items;
} catch (err) {
log(shop, `❌ [${procId}] Error fetching items: ${err.message}`);
return null;
}
}
const AddProductToStore = async (shop, accessToken, product, procId) => {
var results = [];
const SHOP = shop;
const ACCESS_TOKEN = accessToken;
const item = product;
const attrs = item.attributes;
try {
var inventoryData = attrs.inventorydata.inventory
const totalQuantity = Object.values(inventoryData).reduce((sum, val) => sum + val, 0);
//console.log(totalQuantity, "1234567890")
const client = axios.create({
// baseURL: `https://${SHOP}/admin/api/${API_VERSION}/graphql.json`,
baseURL: `https://${SHOP}/admin/api/2024-01/graphql.json`,
headers: {
'X-Shopify-Access-Token': ACCESS_TOKEN,
'Content-Type': 'application/json',
},
});
const client_new = axios.create({
// baseURL: `https://${SHOP}/admin/api/${API_VERSION}/graphql.json`,
baseURL: `https://${SHOP}/admin/api/2025-07/graphql.json`,
headers: {
'X-Shopify-Access-Token': ACCESS_TOKEN,
'Content-Type': 'application/json',
},
});
log(shop, `🛒 [${procId}] Processing product: ${attrs.product_name || attrs.part_number}`);
// 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))
);
// Find or create collections, collect their IDs
const collectionIds = [];
for (const title of collectionTitles) {
log(shop, `🏷️ [${procId}] Handling 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) {
log(shop, `✅ [${procId}] Found existing collection: ${title}`);
collectionIds.push(existing[0].id);
continue;
}
// 2. Otherwise, create it
log(shop, ` [${procId}] Creating new collection: ${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;
log(shop, `✨ [${procId}] Created collection: ${title} (ID: ${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());
// 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}`,
}));
// 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;
log(shop, `🔄 [${procId}] Creating product: ${attrs.product_name}`);
const handle = slugify(item.id)
// const handle = slugify(item.id + "-" + (attrs.mfr_part_number || attrs.product_name))
const searchRes = await client.post('', {
query: `
query {
products(first: 1, query: "handle:${handle}") {
nodes { id handle }
}
}
`
});
// console.log(`[AddProductToStore] Search result for handle "${handle}":`, searchRes.data.data.products);
const exists = searchRes.data?.data?.products?.nodes?.length > 0;
if (exists) {
log(shop, `⏭️ [${procId}] Skipping duplicate product: ${attrs.part_number}`);
results.push({ skippedHandle: attrs.part_number, reason: "handle in use" });
return null;
} else {
// Proceed with productCreate mutation
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: handle,
tags,
collectionsToJoin: collectionIds,
status: "ACTIVE",
},
media: mediaInputs,
},
});
const createProdJson = createProdRes.data;
const prodErrs = createProdJson.data?.productCreate?.userErrors || [];
if (prodErrs.length) {
const taken = prodErrs.some(e => /already in use/i.test(e.message));
if (taken) {
log(shop, `⏭️ [${procId}] Skipping duplicate product: ${attrs.part_number}`);
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) {
log(shop, `⚠️ [${procId}] No variant found 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 || "";
log(shop, `💲 [${procId}] Updating pricing for variant: ${variantId}`);
const weightValue = parseFloat(attrs.dimensions?.[0]?.weight) || 0;
const bulkRes = await client.post('', {
query: `
mutation UpdateProductVariant(
$productId: ID!,
$variants: [ProductVariantsBulkInput!]!
) {
productVariantsBulkUpdate(productId: $productId, variants: $variants) {
productVariants {
id
price
compareAtPrice
barcode
sku
inventoryItem {
measurement {
weight {
value
unit
}
}
tracked
}
}
userErrors {
field
message
}
}
}
`,
variables: {
productId: product.id,
variants: [{
id: variantId,
price,
...(comparePrice !== null && { compareAtPrice: comparePrice }),
...(barcode && { barcode }),
sku: attrs.part_number,
inventoryItem: {
measurement: {
weight: { value: weightValue, unit: "POUNDS" }
},
// tracked: true
}
}]
},
});
log(shop, `🔄 [${procId}] Bulk updating variant: ${variantId}`);
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
log(shop, `📢 [${procId}] Publishing product to Online Store`);
const publicationsRes = await client.post('', {
query: `
query {
publications(first: 10) {
edges {
node {
id
name
}
}
}
}
`
});
const publicationsJson = publicationsRes.data;
const onlineStorePublication = publicationsJson.data.publications.edges.find(
pub => pub.node.name === 'Online Store'
);
const onlineStorePublicationId = onlineStorePublication ? onlineStorePublication.node.id : null;
if (onlineStorePublicationId) {
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;
const publishErrs = publishJson.data.publishablePublish.userErrors;
if (publishErrs.length) {
throw new Error(`Publish errors: ${publishErrs.map(e => e.message).join(", ")}`);
}
log(shop, `🌐 [${procId}] Published product to Online Store`);
} else {
throw new Error("Online Store publication not found.");
}
const costPerItem = parseFloat(attrs.purchase_cost) || 0;
log(shop, `📦 [${procId}] Updating inventory for product`);
const invRes = await client.post('', {
query: `
mutation($id: ID!, $input: InventoryItemUpdateInput!) {
inventoryItemUpdate(id: $id, input: $input) {
inventoryItem {
id
sku
unitCost { amount }
tracked
measurement {
weight {
value
unit
}
}
}
userErrors { field message }
}
}
`,
variables: {
id: inventoryItemId,
input: {
cost: parseFloat(attrs.purchase_cost) || 0,
tracked: true
}
}
});
const invJson = invRes.data;
const invErrs = invJson.data.inventoryItemUpdate.userErrors;
if (invErrs.length) {
throw new Error(`Inventory update errors: ${invErrs.map(e => e.message).join(", ")}`);
}
// const locationsRes = await client.post('', {
// query: `
// query {
// locations(first: 10) {
// edges {
// node {
// id
// name
// }
// }
// }
// }
// `
// });
// const locations = locationsRes.data.data.locations.edges;
// const locationId = locations[0].node.id; // Use your logic to pick the right location
// const locationsRes = await client.post('', {
// query: `
// query {
// locations(first: 20, query: "name:'Data4Autos Distribution'") {
// edges {
// node {
// id
// name
// fulfillmentService {
// serviceName
// }
// }
// }
// }
// }
// `
// });
const locationsRes = await client.post('', {
query: `
query {
locations(first: 10) {
edges {
node {
id
name
}
}
}
}
`
});
const locations = locationsRes.data.data.locations.edges;
log(shop, `Locations: ${JSON.stringify(locations, null, 2)}`);
console.log('Locations:', locations);
const location = locations.find(
loc => loc.node.fulfillmentService && loc.node.fulfillmentService.serviceName === "Data4Autos Distribution"
);
//const locationId = location ? location.node.id : null;
const locationId = "gid://shopify/Location/84790051032"
console.log('Location ID:', locationId);
log(shop, `Locations: ${JSON.stringify(locations, null, 2)}`);
try {
const mutation = `
mutation InventoryActivate($inventoryItemId: ID!, $locationId: ID!) {
inventoryActivate(inventoryItemId: $inventoryItemId, locationId: $locationId) {
inventoryLevel {
id
item { id }
location { id }
quantities(names: ["available"]) {
name
quantity
}
}
userErrors {
field
message
}
}
}
`;
const variables = {
inventoryItemId: inventoryItemId, // e.g. "gid://shopify/InventoryItem/123456789"
locationId: locationId // e.g. "gid://shopify/Location/84790051032"
};
let activateInventoryRes;
try {
activateInventoryRes = await client_new.post('', {
query: mutation,
variables: variables
});
// Print the full response from Shopify
console.log(JSON.stringify(activateInventoryRes.data, null, 2));
log(shop, `✅ [${procId}] Inventory activated for item ${inventoryItemId} at location ${locationId}`);
} catch (error) {
if (error.activateInventoryRes) {
console.error('Error:', error.activateInventoryRes.data);
} else {
console.error('Error:', error.message);
}
}
} catch (error) {
log(shop, `❌ [${procId}] Error activating inventory: ${error.message}`);
}
const mutation = `
mutation InventorySet($input: InventorySetQuantitiesInput!) {
inventorySetQuantities(input: $input) {
inventoryAdjustmentGroup {
createdAt
reason
referenceDocumentUri
changes {
name
delta
}
}
userErrors {
field
message
}
}
}
`;
const variables = {
input: {
name: "available",
reason: "correction",
referenceDocumentUri: "logistics://some.warehouse/take/2023-01-23T13:14:15Z",
ignoreCompareQuantity: true,
quantities: [
{
inventoryItemId: inventoryItemId,
locationId: locationId,
quantity: totalQuantity,
compareQuantity: 1
}
]
}
};
var setInventoryRes
try {
// console.log("newwww")
setInventoryRes = await client_new.post('', {
query: mutation,
variables: variables
});
// Print the full setInventoryRes from Shopify
// console.log(JSON.stringify(setInventoryRes.data, null, 2));
} catch (error) {
if (error.setInventoryRes) {
console.error('Error:', error.setInventoryRes.data);
} else {
console.error('Error:', error.message);
}
}
const setInventoryData = setInventoryRes.data.inventorySetQuantities;
if (setInventoryData.userErrors.length) {
throw new Error(
"Inventory update errors: " +
setInventoryData.userErrors.map(e => e.message).join(", ")
);
}
// Get the updated inventory item from the response
const updatedInventoryItem = invJson.data.inventoryItemUpdate.inventoryItem;
// Collect results
results.push({
productId: product.id,
variant: {
id: variantId,
price: variantNode.price,
compareAtPrice: variantNode.compareAtPrice,
sku: updatedInventoryItem.sku || attrs.part_number || '',
barcode: variantNode.barcode || attrs.barcode || '',
weight: updatedInventoryItem?.measurement?.weight?.value || 0,
weightUnit: updatedInventoryItem?.measurement?.weight?.unit || 'kg',
},
collections: collectionTitles,
tags,
});
log(shop, `✅ [${procId}] Successfully processed product: ${attrs.product_name}`);
return results;
}
} catch (err) {
log(shop, `❌ [${procId}] Error processing product: ${err.message}`);
results.push({
error: `Failed to process item ${item.id}: ${err.message}`,
product: attrs.product_name || attrs.part_number || 'Unknown'
});
return results;
}
}
const GetAllProductsandAddToStore = async (shop, accessToken, brandId, turn14accessToken, procId) => {
log(shop, `🔍 [${procId}] Fetching products for brand ${brandId}`);
const products = await GetAllProductsOfBranch(brandId, turn14accessToken, shop, procId);
if (!products) {
log(shop, `⚠️ [${procId}] No products found for brand ${brandId}`);
return [];
}
// Update total products count
processes[procId].totalProducts = products.length;
processes[procId].processedProducts = 0;
const results = [];
log(shop, `🔄 [${procId}] Processing ${products.length} products`);
for (const [index, item] of products.entries()) {
try {
// Update current product being processed
const attrs = item.attributes;
processes[procId].currentProduct = {
name: attrs.product_name || attrs.part_number || 'Unknown',
number: index + 1,
total: products.length
};
processes[procId].status = `processing (${index + 1}/${products.length})`;
const res = await AddProductToStore(shop, accessToken, item, procId);
if (res) results.push(...res);
// Update processed count
processes[procId].processedProducts = index + 1;
processes[procId].detail = `Processed ${index + 1} of ${products.length} products`;
} catch (err) {
log(shop, `⚠️ [${procId}] Error processing product ${index + 1}: ${err.message}`);
results.push({
error: `Failed to process product ${index + 1}: ${err.message}`,
product: item.attributes.product_name || item.attributes.part_number || 'Unknown'
});
}
}
// Clear current product when done
processes[procId].currentProduct = null;
log(shop, `✅ [${procId}] Completed processing ${results.length} products`);
return results;
}
router.post('/', async (req, res) => {
const { shop, brandID, turn14accessToken } = req.body;
const procId = uuid();
processes[procId] = {
status: 'started',
detail: null,
totalProducts: 0,
processedProducts: 0,
currentProduct: null,
results: []
};
log(shop, `🔔 [${procId}] Starting product import for brand ${brandID}`);
res.json({ processId: procId, status: 'started' });
(async () => {
try {
processes[procId].status = 'fetching_products';
log(shop, `🔍 [${procId}] Fetching token for shop`);
// 1. Get token
const tokenRecord = getToken(shop);
if (!tokenRecord) throw new Error('No token for shop');
processes[procId].status = 'importing_products';
processes[procId].detail = 'Starting product import';
const importResults = await GetAllProductsandAddToStore(shop, tokenRecord.accessToken, brandID, turn14accessToken, procId);
log(shop, `✅ [${procId}] Successfully imported ${importResults.length} products`);
processes[procId].status = 'done';
processes[procId].detail = `Imported ${importResults.length} products`;
processes[procId].results = importResults;
} 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' });
const response = {
status: info.status,
detail: info.detail,
progress: info.totalProducts > 0
? Math.round((info.processedProducts / info.totalProducts) * 100)
: 0,
current: info.currentProduct,
stats: {
total: info.totalProducts,
processed: info.processedProducts,
remaining: info.totalProducts - info.processedProducts
},
results: info.results || []
};
res.json(response);
});
module.exports = router;