import { isValidEan13 } from './barcode' /** * Label-scale barcodes: EAN-13 in the GS1 restricted-circulation range * (prefix 20–29) with the item PLU and a weight or price embedded — the * supermarket produce path where every scan must be zero keystrokes. * Layout: [2-digit prefix][PLU][value][check]; PLU + value = 10 digits. * The layout is per-store scale configuration, hence a profile row. */ export interface ScaleBarcodeProfile { /** Which 2-digit prefixes this store's scales emit, e.g. ['21']. */ prefixes: string[] pluLength: 4 | 5 | 6 valueKind: 'weight' | 'price' /** value → quantity (weight: grams with divisor 1000 → kg) or paise. */ valueDivisor: number } export type ScaleParse = | { kind: 'scale-weight'; plu: string; qty: number } | { kind: 'scale-price'; plu: string; pricePaise: number } | { kind: 'not-scale' } | { kind: 'invalid'; reason: string } export function parseScaleBarcode(code: string, profile: ScaleBarcodeProfile): ScaleParse { if (!/^\d{13}$/.test(code)) return { kind: 'not-scale' } if (!profile.prefixes.includes(code.slice(0, 2))) return { kind: 'not-scale' } if (!isValidEan13(code)) return { kind: 'invalid', reason: 'check-digit' } const valueLength = 10 - profile.pluLength const plu = code.slice(2, 2 + profile.pluLength) const value = Number(code.slice(2 + profile.pluLength, 2 + profile.pluLength + valueLength)) if (profile.valueKind === 'weight') { const qty = value / profile.valueDivisor if (qty <= 0) return { kind: 'invalid', reason: 'zero-weight' } return { kind: 'scale-weight', plu, qty } } return { kind: 'scale-price', plu, pricePaise: value } }