1014 lines
29 KiB
JavaScript
Executable File
1014 lines
29 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 = '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, productCount) => {
|
||
var AllProductsOfBrans = [];
|
||
|
||
try {
|
||
log(shop, `🔍 [${procId}] Fetching products for brand ${brandId}`);
|
||
// const res = await fetch(`https://turn14.data4autos.com/v1/items/brandallitems/${brandId}`, {
|
||
const res = await fetch(`https://turn14.data4autos.com/v1/items/brandallitemswithfitment/${brandId}`, {
|
||
headers: {
|
||
Authorization: `Bearer ${turn14accessToken}`,
|
||
"Content-Type": "application/json",
|
||
},
|
||
});
|
||
const res_data = await res.json();
|
||
const data = res_data.items || [];
|
||
const fitmentTags = res_data.fitmentTags || [];
|
||
// 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, fitmentTags };
|
||
} catch (err) {
|
||
log(shop, `❌ [${procId}] Error fetching items: ${err.message}`);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
const AddProductToStore = async (shop, accessToken, product, procId, fulfillmentServiceId, locationId) => {
|
||
var results = [];
|
||
|
||
const SHOP = shop;
|
||
const ACCESS_TOKEN = accessToken;
|
||
const item = product;
|
||
const attrs = item.attributes;
|
||
|
||
|
||
|
||
const globalUniqueFitmentMap = {
|
||
make: new Set(),
|
||
model: new Set(),
|
||
year: new Set(),
|
||
drive: new Set(),
|
||
baseModel: new Set()
|
||
};
|
||
|
||
// Loop over all processed items
|
||
|
||
const tags = item.attributes?.fitmmentTags;
|
||
|
||
for (const key in globalUniqueFitmentMap) {
|
||
if (tags[key]) {
|
||
tags[key].forEach(value => {
|
||
globalUniqueFitmentMap[key].add(value);
|
||
});
|
||
}
|
||
}
|
||
|
||
|
||
// Convert sets to arrays
|
||
const convertedGlobalUniqueFitmentMap = {};
|
||
for (const key in globalUniqueFitmentMap) {
|
||
convertedGlobalUniqueFitmentMap[key] = Array.from(globalUniqueFitmentMap[key]);
|
||
}
|
||
const fitmentTags = convertedGlobalUniqueFitmentMap;
|
||
|
||
|
||
const allFitmentTagsSet = new Set();
|
||
for (const arr of Object.values(convertedGlobalUniqueFitmentMap)) {
|
||
arr.forEach(val => allFitmentTagsSet.add(val));
|
||
}
|
||
const allFitmentTags = Array.from(allFitmentTagsSet);
|
||
// Now allFitmentTags is a flat array of unique values
|
||
|
||
log(shop, `All Fitment Tags for ${attrs.product_name || attrs.part_number}: ${JSON.stringify(allFitmentTags, null, 2)}`);
|
||
|
||
log(shop, `Fitment Tags for ${attrs.product_name || attrs.part_number}: ${JSON.stringify(fitmentTags, null, 2)}`);
|
||
|
||
|
||
|
||
|
||
|
||
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',
|
||
},
|
||
});
|
||
|
||
|
||
const client_2510 = axios.create({
|
||
// baseURL: `https://${SHOP}/admin/api/${API_VERSION}/graphql.json`,
|
||
baseURL: `https://${SHOP}/admin/api/2025-10/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, ...allFitmentTags].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,
|
||
...allFitmentTags,
|
||
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 createProdRes = await client_2510.post('', {
|
||
query: `
|
||
mutation ProductCreate($product: ProductCreateInput!, $media: [CreateMediaInput!]) {
|
||
productCreate(product: $product, media: $media) {
|
||
product {
|
||
id
|
||
variants(first: 1) {
|
||
nodes {
|
||
id
|
||
inventoryItem { id }
|
||
price
|
||
compareAtPrice
|
||
barcode
|
||
}
|
||
}
|
||
}
|
||
userErrors { field message }
|
||
}
|
||
}
|
||
`,
|
||
variables: {
|
||
product: {
|
||
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 baseprice = parseFloat(attrs.price) || 0;
|
||
|
||
|
||
|
||
|
||
const pricingConfigRes = await client.post('', {
|
||
query: `
|
||
query {
|
||
shop {
|
||
metafield(namespace: "turn14", key: "pricing_config") {
|
||
value
|
||
}
|
||
}
|
||
}
|
||
`
|
||
});
|
||
|
||
let priceType = 'map';
|
||
let percentage = 0;
|
||
|
||
const pricingMf = pricingConfigRes.data?.data?.shop?.metafield;
|
||
if (pricingMf?.value) {
|
||
try {
|
||
const parsed = JSON.parse(pricingMf.value);
|
||
priceType = parsed.priceType || 'map';
|
||
percentage = Number(parsed.percentage) || 0;
|
||
} catch (err) {
|
||
console.error('Failed to parse pricing_config metafield:', err);
|
||
}
|
||
}
|
||
|
||
// 2) Apply your price calculation using the metafield values
|
||
let price = baseprice;
|
||
|
||
if (priceType === 'percentage') {
|
||
price = baseprice + (baseprice * (percentage / 100));
|
||
}
|
||
|
||
|
||
|
||
log(shop, `📢 [${procId}] Calculated price: ${price} (type: ${priceType}, percentage: ${percentage})`);
|
||
|
||
|
||
|
||
|
||
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(", ")}`);
|
||
}
|
||
|
||
console.log("Invemtory ID : ", inventoryItemId)
|
||
console.log("Location ID : ", locationId)
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
log(shop, `⚙️ [${procId}] Assigning variant to fulfillment service`);
|
||
|
||
// const assignVariantMutation = `
|
||
// mutation AssignVariantToFulfillmentService($variantId: ID!, $fulfillmentServiceId: ID!) {
|
||
// productVariantUpdate(input: {
|
||
// id: $variantId,
|
||
// fulfillmentServiceId: $fulfillmentServiceId
|
||
// }) {
|
||
// productVariant {
|
||
// id
|
||
// fulfillmentService {
|
||
// id
|
||
// serviceName
|
||
// }
|
||
// }
|
||
// userErrors {
|
||
// field
|
||
// message
|
||
// }
|
||
// }
|
||
// }
|
||
// `;
|
||
|
||
// const assignVariantVariables = {
|
||
// variantId: variantId, // your variant ID
|
||
// fulfillmentServiceId: fulfillmentServiceId // your fulfillment service ID
|
||
// };
|
||
|
||
// const assignVariantRes = await client.post('', {
|
||
// query: assignVariantMutation,
|
||
// variables: assignVariantVariables
|
||
// });
|
||
// console.log('Assign Variant:', JSON.stringify(assignVariantRes.data, null, 2));
|
||
|
||
|
||
|
||
|
||
|
||
const assignVariantMutation = `
|
||
mutation ProductVariantsBulkUpdate($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {
|
||
productVariantsBulkUpdate(productId: $productId, variants: $variants) {
|
||
productVariants {
|
||
id
|
||
title
|
||
sku
|
||
}
|
||
userErrors {
|
||
field
|
||
message
|
||
}
|
||
}
|
||
}
|
||
`;
|
||
|
||
console.log("Product ID : ", product.id)
|
||
|
||
const assignVariantVariables = {
|
||
productId: product.id, // Replace with your product ID
|
||
variants: [
|
||
{
|
||
id: inventoryItemId, // Replace with your variant ID
|
||
fulfillmentServiceId: fulfillmentServiceId // Replace with your fulfillment service ID
|
||
}
|
||
]
|
||
};
|
||
|
||
const assignVariantRes = await client_new.post('', {
|
||
query: assignVariantMutation,
|
||
variables: assignVariantVariables
|
||
});
|
||
console.log('Assign Variant:', JSON.stringify(assignVariantRes.data, null, 2));
|
||
|
||
|
||
|
||
|
||
// const activateInventoryMutation = `
|
||
// mutation ActivateInventoryItem($inventoryItemId: ID!, $locationId: ID!) {
|
||
// inventoryActivate(inventoryItemId: $inventoryItemId, locationId: $locationId) {
|
||
// inventoryLevel {
|
||
// id
|
||
// quantities(names: ["available"]) {
|
||
// name
|
||
// quantity
|
||
// }
|
||
// item { id }
|
||
// location { id }
|
||
// }
|
||
// userErrors {
|
||
// field
|
||
// message
|
||
// }
|
||
// }
|
||
// }
|
||
// `;
|
||
|
||
// const activateInventoryVariables = {
|
||
// inventoryItemId: inventoryItemId, // your inventory item ID
|
||
// locationId: locationId
|
||
// };
|
||
|
||
// const activateInventoryRes = await client.post('', {
|
||
// query: activateInventoryMutation,
|
||
// variables: activateInventoryVariables
|
||
// });
|
||
// console.log('Activate Inventory:', JSON.stringify(activateInventoryRes.data, null, 2));
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
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, tokens, selectedProductIds, productCount) => {
|
||
const fulfillmentServiceTokens = tokens.fulfillmentService || {}
|
||
const fulfillmentServiceId = fulfillmentServiceTokens.id || null;
|
||
const locationId = fulfillmentServiceTokens.location ? fulfillmentServiceTokens.location.id : null;
|
||
log(shop, `🔍 [${procId}] Fetching products for brand ${brandId}`);
|
||
const products_res = await GetAllProductsOfBranch(brandId, turn14accessToken, shop, procId, productCount);
|
||
|
||
const items = products_res ? products_res.items : [];
|
||
|
||
|
||
|
||
// Update total products count
|
||
|
||
|
||
const results = [];
|
||
|
||
const products = items.filter(item => {
|
||
return selectedProductIds.includes(item.id);
|
||
});
|
||
|
||
|
||
// const globalUniqueFitmentMap = {
|
||
// make: new Set(),
|
||
// model: new Set(),
|
||
// year: new Set(),
|
||
// drive: new Set(),
|
||
// baseModel: new Set()
|
||
// };
|
||
|
||
// // Loop over all processed items
|
||
// for (const item of products) {
|
||
// const tags = item.attributes?.fitmmentTags;
|
||
|
||
// if (!tags) continue;
|
||
|
||
// for (const key in globalUniqueFitmentMap) {
|
||
// if (tags[key]) {
|
||
// tags[key].forEach(value => {
|
||
// globalUniqueFitmentMap[key].add(value);
|
||
// });
|
||
// }
|
||
// }
|
||
// }
|
||
|
||
// // Convert sets to arrays
|
||
// const convertedGlobalUniqueFitmentMap = {};
|
||
// for (const key in globalUniqueFitmentMap) {
|
||
// convertedGlobalUniqueFitmentMap[key] = Array.from(globalUniqueFitmentMap[key]);
|
||
// }
|
||
// const fitmentTags = convertedGlobalUniqueFitmentMap;
|
||
|
||
|
||
// const allFitmentTagsSet = new Set();
|
||
// for (const arr of Object.values(convertedGlobalUniqueFitmentMap)) {
|
||
// arr.forEach(val => allFitmentTagsSet.add(val));
|
||
// }
|
||
// const allFitmentTags = Array.from(allFitmentTagsSet);
|
||
// // Now allFitmentTags is a flat array of unique values
|
||
|
||
// log(shop, `All Fitment Tags: ${JSON.stringify(allFitmentTags, null, 2)}`);
|
||
|
||
// log(shop, `Fitment Tags: ${JSON.stringify(fitmentTags, null, 2)}`);
|
||
|
||
processes[procId].totalProducts = products.length;
|
||
processes[procId].processedProducts = 0;
|
||
log(shop, `🔄 [${procId}] Processing ${products.length} products`);
|
||
if (!products) {
|
||
log(shop, `⚠️ [${procId}] No products found for brand ${brandId}`);
|
||
return [];
|
||
}
|
||
|
||
|
||
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, fulfillmentServiceId, locationId);
|
||
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, productCount, selectedProductIds } = 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
|
||
|
||
|
||
if (!turn14accessToken) throw new Error('No Turn14 access token provided');
|
||
if (!brandID) throw new Error('No brand ID provided');
|
||
if (!shop) throw new Error('No shop provided');
|
||
if (!selectedProductIds) throw new Error('No selected product IDs provided');
|
||
log(shop, `Selected Product IDs: ${selectedProductIds}`);
|
||
// console.log("Selected Product IDs:", selectedProductIds);
|
||
if (!Array.isArray(selectedProductIds) || selectedProductIds.length === 0) {
|
||
throw new Error('Selected product IDs must be a non-empty array');
|
||
}
|
||
|
||
|
||
log(shop, `🔍 [${procId}] Fetching products for brand ${brandID}`);
|
||
|
||
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, tokenRecord, selectedProductIds, productCount);
|
||
|
||
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; |