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

1014 lines
29 KiB
JavaScript
Executable File
Raw Permalink 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, 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;