-- ============================================================================ -- Migration 032 - Modulo Questionari Fornitori (supply chain Art.21.2.d) -- ---------------------------------------------------------------------------- -- PROPOSTA DI DESIGN (NON ancora applicata). Da eseguire su host MySQL. -- -- SOSTITUISCE supplier_questionnaires (mig 027, da qui DEPRECATA/read-only) con -- un modello completo a template versionati + campagne + domande + risposte: -- -- supplier_categories = tassonomia fornitori (preset di sistema + per-org) -- questionnaire_templates = modelli di questionario (per-org, opz. per categoria) -- questionnaire_template_versions= snapshot versionato (questions_snapshot JSON, weight) -- questionnaire_questions = domande "live" del template (ordinabili, pesate) -- questionnaire_campaigns = invio di un template a un fornitore (sostituisce 027) -- questionnaire_answers = risposte puntuali (answer_value JSON-ready) -- -- Retrocompat: i token sq_ gia inviati (027) restano validi grazie alla -- colonna access_token_hash (stesso SHA-256 di supplier_questionnaires.token_hash), -- migrata da mig 035 (migrazione dati). 027 NON viene droppata: resta read-only. -- -- IMPORTANTE (CLAUDE.md / memoria): MySQL 8 Ubuntu NON supporta -- "ADD COLUMN IF NOT EXISTS" / "CREATE INDEX IF NOT EXISTS". Le ALTER usano -- procedure idempotenti su information_schema. CREATE TABLE IF NOT EXISTS ok. -- DELIMITER non funziona via pipe -> usare "source". -- -- Eseguire con: -- mysql -h localhost nis2_agile_db -e "source docs/sql/032_supplier_questionnaires_module.sql" -- -- Ordine di applicazione: 032 -> 033 -> 034 -> 035 (vedi nota in 035). -- ============================================================================ -- 1. Categorie fornitori (preset di sistema = organization_id 0) CREATE TABLE IF NOT EXISTS supplier_categories ( id INT NOT NULL AUTO_INCREMENT, organization_id INT NOT NULL DEFAULT 0 COMMENT '0 = preset di sistema (visibile a tutte le org)', slug VARCHAR(64) NOT NULL COMMENT 'Identificativo stabile, es. cloud_provider', name VARCHAR(255) NOT NULL, description TEXT NULL, is_system TINYINT(1) NOT NULL DEFAULT 0 COMMENT '1 per i preset di sistema', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uq_supcat_org_slug (organization_id, slug), KEY idx_supcat_org (organization_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Tassonomia fornitori: preset di sistema (org 0) + categorie per-org'; -- 2. Template questionario (per-org, opz. legato a una categoria) CREATE TABLE IF NOT EXISTS questionnaire_templates ( id INT NOT NULL AUTO_INCREMENT, organization_id INT NOT NULL, category_id INT NULL COMMENT 'FK supplier_categories: template specifico per categoria', name VARCHAR(255) NOT NULL, description TEXT NULL, current_version VARCHAR(20) NOT NULL DEFAULT '1.0' COMMENT 'Versione live (vedi questionnaire_template_versions)', status ENUM('draft','active','archived') NOT NULL DEFAULT 'draft', pass_threshold TINYINT UNSIGNED NULL COMMENT 'Soglia score 0-100 per esito positivo', is_default TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Template predefinito per la org/categoria', created_by INT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_qt_org (organization_id), KEY idx_qt_category (category_id), KEY idx_qt_org_status (organization_id, status), CONSTRAINT fk_qt_org FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE, CONSTRAINT fk_qt_category FOREIGN KEY (category_id) REFERENCES supplier_categories(id) ON DELETE SET NULL, CONSTRAINT fk_qt_creator FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Modelli di questionario di sicurezza fornitori (per organizzazione)'; -- 3. Versioni template (pattern policy_versions, snapshot domande+weight) CREATE TABLE IF NOT EXISTS questionnaire_template_versions ( id INT NOT NULL AUTO_INCREMENT, template_id INT NOT NULL, organization_id INT NOT NULL COMMENT 'Denormalizzato per filtri/authz', version VARCHAR(20) NOT NULL, questions_snapshot JSON NOT NULL COMMENT 'Snapshot completo domande incl. weight, opzioni, tipo', change_note VARCHAR(255) NULL, created_by INT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uq_qtv_template_version (template_id, version), KEY idx_qtv_template (template_id), KEY idx_qtv_org (organization_id), CONSTRAINT fk_qtv_template FOREIGN KEY (template_id) REFERENCES questionnaire_templates(id) ON DELETE CASCADE, CONSTRAINT fk_qtv_org FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE, CONSTRAINT fk_qtv_creator FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Storico versionato dei template questionario (snapshot domande+weight)'; -- 4. Domande "live" del template (ordinabili, pesate, multi-tipo) CREATE TABLE IF NOT EXISTS questionnaire_questions ( id INT NOT NULL AUTO_INCREMENT, template_id INT NOT NULL, organization_id INT NOT NULL COMMENT 'Denormalizzato per filtri/authz', question_code VARCHAR(50) NULL COMMENT 'Codice stabile opzionale, es. gvsc07_a_access_level', question_text TEXT NOT NULL, question_type ENUM('yes_no_partial','single_choice','multi_choice','scale_1_5','text','number','file') NOT NULL DEFAULT 'yes_no_partial', options JSON NULL COMMENT 'Opzioni per single_choice/multi_choice/scale', weight DECIMAL(6,2) NOT NULL DEFAULT 1.00 COMMENT 'Peso nello scoring', is_required TINYINT(1) NOT NULL DEFAULT 1, order_index INT NOT NULL DEFAULT 0 COMMENT 'Ordinamento di presentazione', nis2_ref VARCHAR(40) NULL COMMENT 'Riferimento normativo es. GV.SC-07.a', vuln_flag VARCHAR(64) NULL COMMENT 'Flag vulnerabilita acceso da risposta negativa', high_criticality_only TINYINT(1) NOT NULL DEFAULT 0 COMMENT '1 = mostra solo a fornitori high/critical', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_qq_template_order (template_id, order_index), KEY idx_qq_org (organization_id), CONSTRAINT fk_qq_template FOREIGN KEY (template_id) REFERENCES questionnaire_templates(id) ON DELETE CASCADE, CONSTRAINT fk_qq_org FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Domande del template questionario fornitori (ordinabili e pesate)'; -- 5. Campagne (SOSTITUISCE supplier_questionnaires di mig 027) CREATE TABLE IF NOT EXISTS questionnaire_campaigns ( id INT NOT NULL AUTO_INCREMENT, organization_id INT NOT NULL, supplier_id INT NOT NULL, template_id INT NULL COMMENT 'Template usato (NULL se importata da 027 legacy)', template_version VARCHAR(20) NULL COMMENT 'Versione template congelata all''invio', access_token_hash CHAR(64) NOT NULL COMMENT 'SHA-256 del token sq_ inviato (retrocompat 027)', status ENUM('draft','sent','in_progress','completed','expired','cancelled') NOT NULL DEFAULT 'sent', score INT NULL COMMENT 'Punteggio 0-100 calcolato dalle risposte', risk_level ENUM('low','medium','high','critical') NULL, answers JSON NULL COMMENT 'Cache aggregata risposte (la verita e in questionnaire_answers)', show_score_to_supplier TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'F23: default OFF', language ENUM('it','en','auto') NOT NULL DEFAULT 'it' COMMENT 'Lingua portale/email', sent_to_email VARCHAR(255) NULL, sent_at DATETIME NULL, due_at DATETIME NULL COMMENT 'Scadenza compilazione', completed_at DATETIME NULL, expires_at DATETIME NULL, reminder_count INT NOT NULL DEFAULT 0, last_reminder_at DATETIME NULL, next_reminder_at DATETIME NULL COMMENT 'Prossimo invio reminder (cron)', reminder_offsets JSON NULL COMMENT 'Offset reminder, es. [-14,-7,-1]', is_recurring TINYINT(1) NOT NULL DEFAULT 0, recurrence_months TINYINT UNSIGNED NULL COMMENT 'Periodicita in mesi, se ricorrente', next_recurrence_at DATETIME NULL COMMENT 'Prossima riemissione (cron)', created_by INT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uq_qc_token (access_token_hash), KEY idx_qc_org (organization_id), KEY idx_qc_supplier (supplier_id), KEY idx_qc_template (template_id), KEY idx_qc_status_reminder (status, next_reminder_at), KEY idx_qc_status_due (status, due_at), KEY idx_qc_status_recurrence (status, next_recurrence_at), CONSTRAINT fk_qc_org FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE, CONSTRAINT fk_qc_supplier FOREIGN KEY (supplier_id) REFERENCES suppliers(id) ON DELETE CASCADE, CONSTRAINT fk_qc_template FOREIGN KEY (template_id) REFERENCES questionnaire_templates(id) ON DELETE SET NULL, CONSTRAINT fk_qc_creator FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Campagne questionario inviate ai fornitori (sostituisce supplier_questionnaires/027)'; -- 6. Risposte puntuali (answer_value JSON-ready per multi_choice/file) CREATE TABLE IF NOT EXISTS questionnaire_answers ( id INT NOT NULL AUTO_INCREMENT, campaign_id INT NOT NULL, organization_id INT NOT NULL COMMENT 'Denormalizzato NOT NULL per filtri/authz', question_id INT NULL COMMENT 'FK domanda live (NULL se domanda legacy/snapshot)', question_code VARCHAR(50) NULL COMMENT 'Codice domanda dallo snapshot, per storicizzazione', answer_value JSON NULL COMMENT 'Valore risposta JSON-ready (scalare/array per multi_choice/file)', answer_text TEXT NULL COMMENT 'Testo libero', file_ref VARCHAR(255) NULL COMMENT 'Riferimento allegato', score_awarded DECIMAL(6,2) NULL COMMENT 'Punteggio attribuito alla risposta', answered_by INT NULL COMMENT 'supplier_user_id che ha risposto (multi-referente)', answered_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uq_qa_campaign_question (campaign_id, question_id), KEY idx_qa_org (organization_id), KEY idx_qa_question (question_id), CONSTRAINT fk_qa_campaign FOREIGN KEY (campaign_id) REFERENCES questionnaire_campaigns(id) ON DELETE CASCADE, CONSTRAINT fk_qa_org FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE, CONSTRAINT fk_qa_question FOREIGN KEY (question_id) REFERENCES questionnaire_questions(id) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Risposte puntuali alle campagne questionario (answer_value JSON-ready, append-only)'; -- 7. Seed idempotente preset di sistema (organization_id = 0) - 10 categorie INSERT INTO supplier_categories (organization_id, slug, name, description, is_system) VALUES (0, 'cloud_provider', 'Cloud Provider (IaaS/PaaS/SaaS)', 'Fornitori di servizi cloud e hosting', 1), (0, 'managed_security', 'Managed Security / MSSP', 'SOC, MDR, gestione sicurezza in outsourcing', 1), (0, 'connectivity_telco', 'Connettivita / Telco', 'Operatori di rete, ISP, connettivita', 1), (0, 'hardware_supplier', 'Fornitore Hardware', 'Apparati di rete, server, dispositivi', 1), (0, 'software_vendor', 'Software Vendor / SaaS applicativo', 'Vendor di software applicativo e licenze', 1), (0, 'professional_services', 'Servizi Professionali / Consulenza', 'Consulenza IT, legale, audit, compliance', 1), (0, 'logistics', 'Logistica e Trasporti', 'Fornitori di logistica, magazzino, spedizioni', 1), (0, 'facility_management', 'Facility Management', 'Gestione immobili, energia, impianti, sicurezza fisica',1), (0, 'datacenter_colocation', 'Data center / Colocation', 'Data center, housing, colocation, infrastruttura fisica',1), (0, 'tic_software_dev', 'TIC / Software Development', 'Sviluppo software su commessa, system integration, ICT', 1) ON DUPLICATE KEY UPDATE name = VALUES(name), description = VALUES(description), is_system = VALUES(is_system), updated_at = CURRENT_TIMESTAMP; -- ROLLBACK (manuale, ordine inverso per le FK): -- DROP TABLE IF EXISTS questionnaire_answers; -- DROP TABLE IF EXISTS questionnaire_campaigns; -- DROP TABLE IF EXISTS questionnaire_questions; -- DROP TABLE IF EXISTS questionnaire_template_versions; -- DROP TABLE IF EXISTS questionnaire_templates; -- DELETE FROM supplier_categories WHERE organization_id = 0 AND is_system = 1; -- DROP TABLE IF EXISTS supplier_categories;