|
|
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 }
|
|
|
}
|