-- Multi-barcode support: one product can carry many GS1 barcodes -- (consumer unit EAN-13/UPC, case ITF-14, regional re-labels, etc.). -- product.gtin is kept as the denormalized "primary" barcode for -- backward compatibility and is mirrored from the is_primary row here. CREATE TABLE product_barcode ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), product_id UUID NOT NULL REFERENCES product(id) ON DELETE CASCADE, gtin VARCHAR(14) NOT NULL, gtin_type VARCHAR(8) NOT NULL DEFAULT 'EAN13', pack_level VARCHAR(8) NOT NULL DEFAULT 'each', region VARCHAR(8), is_primary BOOLEAN NOT NULL DEFAULT false, source_id UUID REFERENCES source(id), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT product_barcode_type_chk CHECK (gtin_type IN ('EAN8','UPC','EAN13','ITF14','GTIN14')), CONSTRAINT product_barcode_pack_chk CHECK (pack_level IN ('each','case','pallet')) ); -- A barcode is globally unique: one code maps to exactly one product. CREATE UNIQUE INDEX idx_product_barcode_gtin ON product_barcode (gtin); CREATE INDEX idx_product_barcode_product ON product_barcode (product_id); -- At most one primary barcode per product. CREATE UNIQUE INDEX idx_product_barcode_primary ON product_barcode (product_id) WHERE is_primary; -- Backfill: lift each product's existing gtin into the new table as primary. INSERT INTO product_barcode (product_id, gtin, gtin_type, pack_level, is_primary) SELECT id, gtin, CASE WHEN length(gtin) = 8 THEN 'EAN8' WHEN length(gtin) = 12 THEN 'UPC' WHEN length(gtin) = 14 THEN 'GTIN14' ELSE 'EAN13' END, 'each', true FROM product WHERE gtin IS NOT NULL AND gtin <> '' ON CONFLICT (gtin) DO NOTHING;