// Tolerant matching of a scanned barcode against stored product codes. // // Printed barcodes differ from the stored value in three ways: // 1. separators — "PZN-15821889" / "PZN - 15821889" vs stored "PZN15821889" // 2. missing prefix — "-15821889" / "15821889" vs stored "PZN15821889" // (a PZN Code39 barcode encodes a leading "-" + the digits, dropping "PZN") // 3. keyboard-layout mangling — a HID scanner emulating a US keyboard on a // German (QWERTZ) system delivers Y and Z swapped, e.g. "X001P8YMNP" // arrives as "X001P8ZMNP". For the A-Z/0-9 codes we match, Y/Z is the // ONLY character the two layouts disagree on — digits are identical, // which is why numeric SKUs/EANs never surfaced this. // // We match in TIERS, exact first and loosest last, so a precise match always // wins before a looser one is even tried: // 'exact' — trim + uppercase equality // 'strip' — also remove spaces + hyphens (handles case 1) // 'suffix' — strip, then allow a missing leading (handles case 2) // ALPHABETIC prefix in front of a numeric code // 'layout' — strip, then compare with Y↔Z swapped on the (handles case 3) // scanned side // // Looser tiers can only ADD matches, never remove the exact ones. export const SCAN_TIERS = ['exact', 'strip', 'suffix', 'layout'] as const // QWERTY↔QWERTZ: swap Y and Z (the only A-Z/0-9 difference between layouts). const swapYZ = (s: string): string => s.replace(/[YZ]/g, c => (c === 'Y' ? 'Z' : 'Y')) // Trim + uppercase; with { strip:true } also remove spaces + hyphens, so a // printed barcode "PZN-15821889" / "PZN - 15821889" matches stored "PZN15821889". export const normalizeScanCode = (s: any, { strip = false }: { strip?: boolean } = {}): string => { let v = String(s ?? '').trim().toUpperCase() if (strip) v = v.replace(/[\s-]+/g, '') return v } // Compare one scanned code to one stored code at a given tolerance tier. const codeEqAtTier = (scanned: any, stored: any, tier: string): boolean => { const strip = tier !== 'exact' const a = normalizeScanCode(scanned, { strip }) const b = normalizeScanCode(stored, { strip }) if (!a || !b) return false if (a === b) return true if (tier === 'layout') return swapYZ(a) === b if (tier !== 'suffix') return false // suffix tier: whichever side is a pure number (>= 6 digits), the other must be // that same number behind an ALPHABETIC-only prefix, e.g. "PZN" + "15821889". const num = /^\d{6,}$/.test(a) ? a : (/^\d{6,}$/.test(b) ? b : null) if (!num) return false const full = num === a ? b : a return full.endsWith(num) && /^[A-Z]+$/.test(full.slice(0, full.length - num.length)) } // Does the scanned code match ANY of the given stored code strings at this tier? export const scanMatchesCodes = (scanned: any, codes: any[], { tier = 'strip' }: { tier?: string } = {}): boolean => { if (!String(scanned ?? '').trim()) return false return (codes || []).some(c => c && codeEqAtTier(scanned, c, tier)) } // Convenience for a nested iDempiere M_Product_ID object. export const scanMatchesProduct = (scanned: any, product: any, opts: { tier?: string } = {}): boolean => { if (!product) return false return scanMatchesCodes( scanned, [product.SKU, product.UPC, product.Value, product.mpn, product.MPN, product.EAN, product.ASIN, product.asin, product.fnsku, product.FNSKU], opts ) } // True if the scanned code matches the given codes at ANY tier — for a single // candidate check, where there is no other candidate to collide with. export const scanMatchesAnyTier = (scanned: any, codes: any[]): boolean => SCAN_TIERS.some(tier => scanMatchesCodes(scanned, codes, { tier })) // Find matching items in a list — tries each tier in order and returns the // matches from the FIRST tier that yields any (exact > strip > suffix). export const findScanMatches = (items: T[], scanned: any, getCodes: (i: T) => any[]): T[] => { const list = items || [] for (const tier of SCAN_TIERS) { const hits = list.filter(i => scanMatchesCodes(scanned, getCodes(i), { tier })) if (hits.length) return hits } return [] }