material_manage_system/scripts/checks/company-master-data-integrity.sql
2026-10-08 17:04:09 +08:00

123 lines
9.8 KiB
SQL

-- MySQL 8.0. Run in the target database before upgrading, with application writes stopped.
-- Every row in the final result is a BLOCKER: repair with business confirmation.
-- This check works on legacy and upgraded schemas; inferred dimensions never use company 1.
-- Canonical source: after edits run pwsh -NoProfile -File scripts/sync-company-integrity.ps1
-- Verify the self-contained migration with the same command plus -Check.
-- BEGIN COMPANY INTEGRITY CORE
DROP TEMPORARY TABLE IF EXISTS tmp_company_application_dimensions;
CREATE TEMPORARY TABLE tmp_company_application_dimensions AS
SELECT a.id, COUNT(ai.id) item_count, COUNT(DISTINCT m.organization_id) organization_count,
MIN(m.organization_id) organization_id, COUNT(m.id) material_count
FROM material_applications a LEFT JOIN application_items ai ON ai.application_id=a.id
LEFT JOIN materials m ON m.id=ai.material_id GROUP BY a.id;
DROP TEMPORARY TABLE IF EXISTS tmp_company_log_dimensions;
CREATE TEMPORARY TABLE tmp_company_log_dimensions AS
SELECT s.id, o.organization_id, o.warehouse_id, o.location_id
FROM stock_logs s JOIN inbound_orders o ON s.related_order_type='inbound' AND s.related_order_id=o.id
JOIN inbound_items i ON i.inbound_order_id=o.id AND i.material_id=s.material_id
WHERE s.type='in' AND s.quantity=i.quantity
UNION
SELECT s.id, o.organization_id, o.warehouse_id, o.location_id
FROM stock_logs s JOIN outbound_orders o ON s.related_order_type='outbound' AND s.related_order_id=o.id
JOIN outbound_items i ON i.outbound_order_id=o.id AND i.material_id=s.material_id
WHERE s.type='out' AND s.quantity=i.quantity
UNION
SELECT s.id, d.organization_id, b.warehouse_id, b.location_id
FROM stock_logs s JOIN material_applications a ON s.related_order_type='application' AND s.related_order_id=a.id
JOIN tmp_company_application_dimensions d ON d.id=a.id AND d.item_count>0 AND d.organization_count=1 AND d.material_count=d.item_count
JOIN application_items i ON i.application_id=a.id AND i.material_id=s.material_id
JOIN inventory_balances b ON b.organization_id=d.organization_id AND b.material_id=s.material_id
JOIN warehouses w ON w.id=b.warehouse_id AND w.organization_id=d.organization_id
JOIN warehouse_locations l ON l.id=b.location_id AND l.warehouse_id=w.id
WHERE s.type='out' AND a.status='approved' AND s.quantity=i.quantity;
DROP TEMPORARY TABLE IF EXISTS tmp_company_integrity_anomalies;
CREATE TEMPORARY TABLE tmp_company_integrity_anomalies (
issue VARCHAR(100) NOT NULL, entity_id BIGINT NOT NULL, detail VARCHAR(500) NOT NULL
);
INSERT INTO tmp_company_integrity_anomalies
SELECT 'category_parent',c.id,'Parent category belongs to another company or is missing'
FROM material_categories c LEFT JOIN material_categories p ON p.id=c.parent_id
WHERE c.parent_id IS NOT NULL AND (p.id IS NULL OR p.organization_id<>c.organization_id);
INSERT INTO tmp_company_integrity_anomalies
SELECT 'material_category_unit_type',m.id,'Material category, base unit or type belongs to another company or is missing'
FROM materials m LEFT JOIN material_categories c ON c.id=m.category_id
LEFT JOIN units u ON u.id=m.base_unit_id LEFT JOIN material_types mt ON mt.id=m.material_type_id
WHERE c.id IS NULL OR c.organization_id<>m.organization_id
OR (m.base_unit_id IS NOT NULL AND (u.id IS NULL OR u.organization_id<>m.organization_id))
OR (m.material_type_id IS NOT NULL AND (mt.id IS NULL OR mt.organization_id<>m.organization_id));
INSERT INTO tmp_company_integrity_anomalies
SELECT 'rule_category',r.id,'Code rule category belongs to another company or is missing'
FROM code_rules r LEFT JOIN material_categories c ON c.id=r.category_id
WHERE c.id IS NULL OR c.organization_id<>r.organization_id;
INSERT INTO tmp_company_integrity_anomalies
SELECT 'duplicate_default',user_id,'User has more than one default organization'
FROM user_organizations WHERE is_default<>0 GROUP BY user_id HAVING COUNT(*)>1;
INSERT INTO tmp_company_integrity_anomalies
SELECT 'user_without_access',u.id,'User has no enabled company grant and no enabled super_admin role'
FROM users u WHERE NOT EXISTS(SELECT 1 FROM user_organizations g JOIN organizations o ON o.id=g.organization_id AND o.status='active' WHERE g.user_id=u.id)
AND NOT EXISTS(SELECT 1 FROM user_roles ur JOIN roles r ON r.id=ur.role_id AND r.code='super_admin' AND r.enabled=1 WHERE ur.user_id=u.id);
INSERT INTO tmp_company_integrity_anomalies
SELECT 'multiple_warehouses',organization_id,'Company has multiple warehouses (including inactive ones)'
FROM warehouses GROUP BY organization_id HAVING COUNT(*)>1;
INSERT INTO tmp_company_integrity_anomalies
SELECT 'multiple_locations',warehouse_id,'Warehouse has multiple locations (including inactive ones)'
FROM warehouse_locations GROUP BY warehouse_id HAVING COUNT(*)>1;
INSERT INTO tmp_company_integrity_anomalies
SELECT 'inventory_dimensions',b.id,'Inventory material, company, warehouse or location do not agree'
FROM inventory_balances b LEFT JOIN materials m ON m.id=b.material_id LEFT JOIN warehouses w ON w.id=b.warehouse_id
LEFT JOIN warehouse_locations l ON l.id=b.location_id LEFT JOIN organizations o ON o.id=b.organization_id
WHERE o.id IS NULL OR m.id IS NULL OR w.id IS NULL OR l.id IS NULL OR m.organization_id<>b.organization_id OR w.organization_id<>b.organization_id OR l.warehouse_id<>b.warehouse_id;
INSERT INTO tmp_company_integrity_anomalies
SELECT 'warning_dimensions',r.id,'Warning material, company or warehouse do not agree'
FROM stock_warning_rules r LEFT JOIN materials m ON m.id=r.material_id LEFT JOIN warehouses w ON w.id=r.warehouse_id
WHERE m.id IS NULL OR w.id IS NULL OR m.organization_id<>r.organization_id OR w.organization_id<>r.organization_id;
INSERT INTO tmp_company_integrity_anomalies
SELECT 'inbound_dimensions',o.id,'Inbound company, warehouse, location, materials or actual inventory dimensions do not agree'
FROM inbound_orders o LEFT JOIN warehouses w ON w.id=o.warehouse_id LEFT JOIN warehouse_locations l ON l.id=o.location_id
WHERE w.id IS NULL OR l.id IS NULL OR w.organization_id<>o.organization_id OR l.warehouse_id<>o.warehouse_id
OR NOT EXISTS(SELECT 1 FROM inbound_items i WHERE i.inbound_order_id=o.id)
OR EXISTS(SELECT 1 FROM inbound_items i LEFT JOIN materials m ON m.id=i.material_id
WHERE i.inbound_order_id=o.id AND (m.id IS NULL OR m.organization_id<>o.organization_id
OR NOT EXISTS(SELECT 1 FROM inventory_balances b WHERE b.material_id=i.material_id AND b.organization_id=o.organization_id AND b.warehouse_id=o.warehouse_id AND b.location_id=o.location_id)));
INSERT INTO tmp_company_integrity_anomalies
SELECT 'outbound_dimensions',o.id,'Outbound company, warehouse, location, materials or actual inventory dimensions do not agree'
FROM outbound_orders o LEFT JOIN warehouses w ON w.id=o.warehouse_id LEFT JOIN warehouse_locations l ON l.id=o.location_id
WHERE w.id IS NULL OR l.id IS NULL OR w.organization_id<>o.organization_id OR l.warehouse_id<>o.warehouse_id
OR NOT EXISTS(SELECT 1 FROM outbound_items i WHERE i.outbound_order_id=o.id)
OR EXISTS(SELECT 1 FROM outbound_items i LEFT JOIN materials m ON m.id=i.material_id
WHERE i.outbound_order_id=o.id AND (m.id IS NULL OR m.organization_id<>o.organization_id
OR NOT EXISTS(SELECT 1 FROM inventory_balances b WHERE b.material_id=i.material_id AND b.organization_id=o.organization_id AND b.warehouse_id=o.warehouse_id AND b.location_id=o.location_id)));
INSERT INTO tmp_company_integrity_anomalies
SELECT 'application_dimensions',id,'Application has empty, missing or mixed-company material details'
FROM tmp_company_application_dimensions WHERE item_count=0 OR organization_count<>1 OR material_count<>item_count;
INSERT INTO tmp_company_integrity_anomalies
SELECT 'unattributable_stock_log',s.id,'Log has no unique verified material/document/company/warehouse/location association'
FROM stock_logs s LEFT JOIN tmp_company_log_dimensions d ON d.id=s.id
LEFT JOIN materials m ON m.id=s.material_id LEFT JOIN warehouses w ON w.id=d.warehouse_id LEFT JOIN warehouse_locations l ON l.id=d.location_id
GROUP BY s.id,m.id,m.organization_id
HAVING COUNT(d.id)<>1 OR COUNT(m.id)=0 OR SUM(d.organization_id<>m.organization_id OR w.organization_id<>d.organization_id OR l.warehouse_id<>d.warehouse_id)>0;
-- The same check file must work before the columns exist and after backfill.
SET @company_check_sql = IF(
(SELECT COUNT(*) FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='stock_logs' AND column_name IN ('organization_id','warehouse_id','location_id'))=3,
'INSERT INTO tmp_company_integrity_anomalies SELECT ''stored_stock_log_dimensions'',s.id,''Stored log dimensions differ from verified source document'' FROM stock_logs s LEFT JOIN tmp_company_log_dimensions d ON d.id=s.id WHERE d.id IS NULL OR s.organization_id IS NULL OR s.warehouse_id IS NULL OR s.location_id IS NULL OR s.organization_id<>d.organization_id OR s.warehouse_id<>d.warehouse_id OR s.location_id<>d.location_id',
'DO 0');
PREPARE company_check FROM @company_check_sql;
EXECUTE company_check;
DEALLOCATE PREPARE company_check;
SET @company_check_sql = IF(
(SELECT COUNT(*) FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='material_applications' AND column_name='organization_id')=1,
'INSERT INTO tmp_company_integrity_anomalies SELECT ''stored_application_dimensions'',a.id,''Stored application company differs from verified detail materials'' FROM material_applications a JOIN tmp_company_application_dimensions d ON d.id=a.id WHERE a.organization_id IS NULL OR a.organization_id<>d.organization_id',
'DO 0');
PREPARE company_check FROM @company_check_sql;
EXECUTE company_check;
DEALLOCATE PREPARE company_check;
-- END COMPANY INTEGRITY CORE
SELECT issue, entity_id, detail FROM tmp_company_integrity_anomalies ORDER BY issue,entity_id;
-- Counts are also review evidence, even when zero or one per dimension.
SELECT organization_id,COUNT(*) warehouse_count FROM warehouses GROUP BY organization_id;
SELECT warehouse_id,COUNT(*) location_count FROM warehouse_locations GROUP BY warehouse_id;