Add UPC generation, add automatic correction of UPC check digits, properly deal with already existing UPCs, add in final results display after submitting with option to fix errored products
This commit is contained in:
@@ -955,6 +955,155 @@ router.get('/search-products', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
const UPC_SUPPLIER_PREFIX_LEADING_DIGIT = '4';
|
||||
const UPC_MAX_SEQUENCE = 99999;
|
||||
const UPC_RESERVATION_TTL = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
function buildSupplierPrefix(supplierId) {
|
||||
const numericId = Number.parseInt(String(supplierId), 10);
|
||||
if (Number.isNaN(numericId) || numericId < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const padded = String(numericId).padStart(5, '0');
|
||||
const prefix = `${UPC_SUPPLIER_PREFIX_LEADING_DIGIT}${padded}`;
|
||||
return prefix.length === 6 ? prefix : null;
|
||||
}
|
||||
|
||||
function calculateUpcCheckDigit(upcWithoutCheckDigit) {
|
||||
if (!/^\d{11}$/.test(upcWithoutCheckDigit)) {
|
||||
throw new Error('UPC body must be 11 numeric characters');
|
||||
}
|
||||
|
||||
let sum = 0;
|
||||
for (let i = 0; i < upcWithoutCheckDigit.length; i += 1) {
|
||||
const digit = Number.parseInt(upcWithoutCheckDigit[i], 10);
|
||||
sum += (i % 2 === 0) ? digit * 3 : digit;
|
||||
}
|
||||
|
||||
const mod = sum % 10;
|
||||
return mod === 0 ? 0 : 10 - mod;
|
||||
}
|
||||
|
||||
const upcReservationCache = new Map();
|
||||
const upcGenerationLocks = new Map();
|
||||
|
||||
function getReservedSequence(prefix) {
|
||||
const entry = upcReservationCache.get(prefix);
|
||||
if (!entry) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (Date.now() > entry.expiresAt) {
|
||||
upcReservationCache.delete(prefix);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return entry.lastSequence;
|
||||
}
|
||||
|
||||
function setReservedSequence(prefix, sequence) {
|
||||
upcReservationCache.set(prefix, {
|
||||
lastSequence: sequence,
|
||||
expiresAt: Date.now() + UPC_RESERVATION_TTL
|
||||
});
|
||||
}
|
||||
|
||||
async function runWithSupplierLock(prefix, task) {
|
||||
const previous = upcGenerationLocks.get(prefix) || Promise.resolve();
|
||||
const chained = previous.catch(() => {}).then(() => task());
|
||||
upcGenerationLocks.set(prefix, chained);
|
||||
|
||||
try {
|
||||
return await chained;
|
||||
} finally {
|
||||
if (upcGenerationLocks.get(prefix) === chained) {
|
||||
upcGenerationLocks.delete(prefix);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
router.post('/generate-upc', async (req, res) => {
|
||||
const { supplierId, increment } = req.body || {};
|
||||
|
||||
if (supplierId === undefined || supplierId === null || String(supplierId).trim() === '') {
|
||||
return res.status(400).json({ error: 'Supplier ID is required to generate a UPC' });
|
||||
}
|
||||
|
||||
const supplierPrefix = buildSupplierPrefix(supplierId);
|
||||
if (!supplierPrefix) {
|
||||
return res.status(400).json({ error: 'Supplier ID must be a non-negative number with at most 5 digits' });
|
||||
}
|
||||
|
||||
const step = Number.parseInt(increment, 10);
|
||||
const sequenceIncrement = Number.isNaN(step) || step < 1 ? 1 : step;
|
||||
|
||||
try {
|
||||
const result = await runWithSupplierLock(supplierPrefix, async () => {
|
||||
const { connection } = await getDbConnection();
|
||||
|
||||
const [rows] = await connection.query(
|
||||
`SELECT CAST(SUBSTRING(upc,7,5) AS UNSIGNED) AS num
|
||||
FROM products
|
||||
WHERE LEFT(upc, 6) = ? AND LENGTH(upc) = 12
|
||||
ORDER BY num DESC
|
||||
LIMIT 1`,
|
||||
[supplierPrefix]
|
||||
);
|
||||
|
||||
const lastSequenceFromDb = rows && rows.length > 0 && rows[0].num !== null
|
||||
? Number.parseInt(rows[0].num, 10) || 0
|
||||
: 0;
|
||||
|
||||
const cachedSequence = getReservedSequence(supplierPrefix);
|
||||
const baselineSequence = Math.max(lastSequenceFromDb, cachedSequence);
|
||||
|
||||
let nextSequence = baselineSequence + sequenceIncrement;
|
||||
let candidateUpc = null;
|
||||
let attempts = 0;
|
||||
|
||||
while (attempts < 10 && nextSequence <= UPC_MAX_SEQUENCE) {
|
||||
const sequencePart = String(nextSequence).padStart(5, '0');
|
||||
const upcBody = `${supplierPrefix}${sequencePart}`;
|
||||
const checkDigit = calculateUpcCheckDigit(upcBody);
|
||||
const fullUpc = `${upcBody}${checkDigit}`;
|
||||
|
||||
const [existing] = await connection.query(
|
||||
'SELECT 1 FROM products WHERE upc = ? LIMIT 1',
|
||||
[fullUpc]
|
||||
);
|
||||
|
||||
if (!existing || existing.length === 0) {
|
||||
candidateUpc = { upc: fullUpc, sequence: nextSequence };
|
||||
break;
|
||||
}
|
||||
|
||||
nextSequence += 1;
|
||||
attempts += 1;
|
||||
}
|
||||
|
||||
if (!candidateUpc) {
|
||||
const reason = nextSequence > UPC_MAX_SEQUENCE
|
||||
? 'UPC range exhausted for this supplier'
|
||||
: 'Unable to find an available UPC';
|
||||
const error = new Error(reason);
|
||||
error.status = 409;
|
||||
throw error;
|
||||
}
|
||||
|
||||
setReservedSequence(supplierPrefix, candidateUpc.sequence);
|
||||
return candidateUpc.upc;
|
||||
});
|
||||
|
||||
return res.json({ success: true, upc: result });
|
||||
} catch (error) {
|
||||
console.error('Error generating UPC:', error);
|
||||
const status = error.status && Number.isInteger(error.status) ? error.status : 500;
|
||||
const message = status === 500 ? 'Failed to generate UPC' : error.message;
|
||||
return res.status(status).json({ error: message, details: status === 500 ? error.message : undefined });
|
||||
}
|
||||
});
|
||||
|
||||
// Endpoint to check UPC and generate item number
|
||||
router.get('/check-upc-and-generate-sku', async (req, res) => {
|
||||
const { upc, supplierId } = req.query;
|
||||
@@ -1149,4 +1298,4 @@ router.get('/product-categories/:pid', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
module.exports = router;
|
||||
|
||||
Reference in New Issue
Block a user