-- Global vendor partnumber mapping foundation with backward compatibility. ALTER TABLE lot_partnumbers ADD COLUMN IF NOT EXISTS vendor VARCHAR(255) NOT NULL DEFAULT '' AFTER partnumber; CREATE TABLE IF NOT EXISTS qt_lot_bundles ( bundle_lot_name VARCHAR(255) NOT NULL, is_active TINYINT(1) NOT NULL DEFAULT 1, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (bundle_lot_name) ); CREATE TABLE IF NOT EXISTS qt_lot_bundle_items ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, bundle_lot_name VARCHAR(255) NOT NULL, lot_name VARCHAR(255) NOT NULL, qty DECIMAL(14,6) NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uq_qt_lot_bundle_items_bundle_lot (bundle_lot_name, lot_name), INDEX idx_qt_lot_bundle_items_lot_name (lot_name) ); CREATE TABLE IF NOT EXISTS qt_vendor_partnumber_seen ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, source_type VARCHAR(32) NOT NULL, vendor VARCHAR(255) NOT NULL DEFAULT '', partnumber VARCHAR(255) NOT NULL, description VARCHAR(10000) NULL, last_seen_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, is_ignored TINYINT(1) NOT NULL DEFAULT 0, ignored_at DATETIME NULL, ignored_by VARCHAR(100) NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uq_qt_vendor_partnumber_seen_source_key (source_type, vendor, partnumber), INDEX idx_qt_vendor_partnumber_seen_vendor_partnumber (vendor, partnumber), INDEX idx_qt_vendor_partnumber_seen_ignored (is_ignored) ); CREATE TABLE IF NOT EXISTS qt_bom ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, external_bom_id VARCHAR(255) NOT NULL, source_system VARCHAR(100) NOT NULL, payload JSON NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uq_qt_bom_source_external (source_system, external_bom_id) ); ALTER TABLE qt_configurations ADD COLUMN IF NOT EXISTS bom_id BIGINT UNSIGNED NULL AFTER competitor_pricelist_id; -- Convert legacy many-to-one rows into bundle lots so (vendor,partnumber) can be unique. CREATE TEMPORARY TABLE tmp_lp_dups AS SELECT vendor, partnumber, CONCAT('BND_', UPPER(SUBSTRING(MD5(CONCAT(vendor, '|', partnumber)), 1, 20))) AS bundle_lot_name, MAX(description) AS description FROM lot_partnumbers GROUP BY vendor, partnumber HAVING COUNT(*) > 1; INSERT INTO lot (lot_name, lot_description, lot_category) SELECT d.bundle_lot_name, CONCAT('Auto-generated bundle for ', d.partnumber, IF(d.vendor = '', '', CONCAT(' (', d.vendor, ')'))), 'BUNDLE' FROM tmp_lp_dups d LEFT JOIN lot l ON l.lot_name = d.bundle_lot_name WHERE l.lot_name IS NULL; INSERT INTO qt_lot_bundles (bundle_lot_name, is_active) SELECT d.bundle_lot_name, 1 FROM tmp_lp_dups d LEFT JOIN qt_lot_bundles b ON b.bundle_lot_name = d.bundle_lot_name WHERE b.bundle_lot_name IS NULL; INSERT INTO qt_lot_bundle_items (bundle_lot_name, lot_name, qty) SELECT d.bundle_lot_name, lp.lot_name, 1 FROM tmp_lp_dups d INNER JOIN lot_partnumbers lp ON lp.vendor = d.vendor AND lp.partnumber = d.partnumber WHERE TRIM(lp.lot_name) <> '' ON DUPLICATE KEY UPDATE qty = VALUES(qty), updated_at = CURRENT_TIMESTAMP; DELETE lp FROM lot_partnumbers lp INNER JOIN tmp_lp_dups d ON lp.vendor = d.vendor AND lp.partnumber = d.partnumber; INSERT INTO lot_partnumbers (vendor, partnumber, lot_name, description) SELECT vendor, partnumber, bundle_lot_name, description FROM tmp_lp_dups; DROP TEMPORARY TABLE IF EXISTS tmp_lp_dups; CREATE UNIQUE INDEX uq_lot_partnumbers_vendor_partnumber ON lot_partnumbers (vendor, partnumber); CREATE INDEX idx_lot_partnumbers_vendor_partnumber ON lot_partnumbers (vendor, partnumber); -- Add optional FKs if not present. SET @fk_cfg_bom_exists := ( SELECT COUNT(*) FROM information_schema.REFERENTIAL_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'fk_qt_configurations_bom' ); SET @fk_cfg_bom_sql := IF( @fk_cfg_bom_exists > 0, 'SELECT 1', 'ALTER TABLE qt_configurations ADD CONSTRAINT fk_qt_configurations_bom FOREIGN KEY (bom_id) REFERENCES qt_bom(id) ON DELETE SET NULL' ); PREPARE stmt_cfg_bom FROM @fk_cfg_bom_sql; EXECUTE stmt_cfg_bom; DEALLOCATE PREPARE stmt_cfg_bom; SET @fk_bundle_exists := ( SELECT COUNT(*) FROM information_schema.REFERENTIAL_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'fk_qt_lot_bundles_lot' ); SET @fk_bundle_sql := IF( @fk_bundle_exists > 0, 'SELECT 1', 'ALTER TABLE qt_lot_bundles ADD CONSTRAINT fk_qt_lot_bundles_lot FOREIGN KEY (bundle_lot_name) REFERENCES lot(lot_name) ON DELETE CASCADE' ); PREPARE stmt_bundle FROM @fk_bundle_sql; EXECUTE stmt_bundle; DEALLOCATE PREPARE stmt_bundle; SET @fk_bundle_item_bundle_exists := ( SELECT COUNT(*) FROM information_schema.REFERENTIAL_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'fk_qt_lot_bundle_items_bundle' ); SET @fk_bundle_item_bundle_sql := IF( @fk_bundle_item_bundle_exists > 0, 'SELECT 1', 'ALTER TABLE qt_lot_bundle_items ADD CONSTRAINT fk_qt_lot_bundle_items_bundle FOREIGN KEY (bundle_lot_name) REFERENCES qt_lot_bundles(bundle_lot_name) ON DELETE CASCADE' ); PREPARE stmt_bundle_item_bundle FROM @fk_bundle_item_bundle_sql; EXECUTE stmt_bundle_item_bundle; DEALLOCATE PREPARE stmt_bundle_item_bundle; SET @fk_bundle_item_lot_exists := ( SELECT COUNT(*) FROM information_schema.REFERENTIAL_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND CONSTRAINT_NAME = 'fk_qt_lot_bundle_items_lot' ); SET @fk_bundle_item_lot_sql := IF( @fk_bundle_item_lot_exists > 0, 'SELECT 1', 'ALTER TABLE qt_lot_bundle_items ADD CONSTRAINT fk_qt_lot_bundle_items_lot FOREIGN KEY (lot_name) REFERENCES lot(lot_name) ON DELETE RESTRICT' ); PREPARE stmt_bundle_item_lot FROM @fk_bundle_item_lot_sql; EXECUTE stmt_bundle_item_lot; DEALLOCATE PREPARE stmt_bundle_item_lot;