-- Migration 050: User-Company associations (multi-company support) -- Allows one user to be associated with multiple companies -- Maintains backward compatibility via trigger syncing users.company_id BEGIN; -- 1. Create user_companies table CREATE TABLE IF NOT EXISTS user_companies ( id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, company_id INTEGER NOT NULL REFERENCES companies(id) ON DELETE CASCADE, role VARCHAR(20) NOT NULL DEFAULT 'MANAGER', is_primary BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), UNIQUE(user_id, company_id) ); -- 2. Indexes CREATE INDEX IF NOT EXISTS idx_user_companies_user_id ON user_companies(user_id); CREATE INDEX IF NOT EXISTS idx_user_companies_company_id ON user_companies(company_id); -- No partial unique index on is_primary - enforced by BEFORE trigger instead -- (AFTER trigger can't clear old primary before unique constraint check) -- 3. Migrate existing data from users table INSERT INTO user_companies (user_id, company_id, role, is_primary, created_at) SELECT id, company_id, COALESCE(NULLIF(company_role, 'NONE'), 'MANAGER'), TRUE, COALESCE(created_at, NOW()) FROM users WHERE company_id IS NOT NULL ON CONFLICT (user_id, company_id) DO NOTHING; -- 4. BEFORE trigger: enforce single primary + clear others CREATE OR REPLACE FUNCTION sync_user_primary_company() RETURNS TRIGGER AS $$ BEGIN -- On INSERT or UPDATE with is_primary = TRUE IF TG_OP IN ('INSERT', 'UPDATE') AND NEW.is_primary = TRUE THEN -- Clear other primary flags for this user BEFORE the row is written UPDATE user_companies SET is_primary = FALSE, updated_at = NOW() WHERE user_id = NEW.user_id AND id != NEW.id AND is_primary = TRUE; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS trg_sync_user_primary_company ON user_companies; CREATE TRIGGER trg_sync_user_primary_company BEFORE INSERT OR UPDATE ON user_companies FOR EACH ROW EXECUTE FUNCTION sync_user_primary_company(); -- 5. AFTER trigger: sync to users table CREATE OR REPLACE FUNCTION sync_user_primary_to_users() RETURNS TRIGGER AS $$ BEGIN -- On INSERT or UPDATE with is_primary = TRUE -> sync to users IF TG_OP IN ('INSERT', 'UPDATE') AND NEW.is_primary = TRUE THEN UPDATE users SET company_id = NEW.company_id, company_role = NEW.role WHERE id = NEW.user_id; END IF; -- On DELETE of primary association IF TG_OP = 'DELETE' AND OLD.is_primary = TRUE THEN -- Try to promote another association to primary UPDATE user_companies SET is_primary = TRUE, updated_at = NOW() WHERE id = ( SELECT id FROM user_companies WHERE user_id = OLD.user_id AND id != OLD.id ORDER BY created_at ASC LIMIT 1 ); -- If no other association exists, clear users table IF NOT FOUND THEN UPDATE users SET company_id = NULL, company_role = 'NONE' WHERE id = OLD.user_id; END IF; END IF; IF TG_OP = 'DELETE' THEN RETURN OLD; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS trg_sync_user_primary_to_users ON user_companies; CREATE TRIGGER trg_sync_user_primary_to_users AFTER INSERT OR UPDATE OR DELETE ON user_companies FOR EACH ROW EXECUTE FUNCTION sync_user_primary_to_users(); -- 6. Permissions GRANT ALL ON TABLE user_companies TO nordabiz_app; GRANT USAGE, SELECT ON SEQUENCE user_companies_id_seq TO nordabiz_app; COMMIT;