nis2-agile/docs/sql/032_supplier_questionnaires_module.sql
DevEnv nis2-agile 5c7ed9abcb [FIX] Import fornitori: valida scope org di category_id + allinea header migrazioni 032/033
- bulkUpsertSuppliers: il ramo category_id esplicito (import API/CSV) ora verifica
  che la categoria sia un preset (org 0) o della stessa org, come gia' fa il ramo
  category_slug. Evita di scrivere suppliers.category_id di un'altra org (dato sporco
  cross-org). Finding review multi-agente (MINORE, correttezza dati).
- docs/sql/032,033: header "PROPOSTA DI DESIGN (NON applicata)" -> "APPLICATA su
  produzione 2026-05-31" (sono effettivamente applicate). Evita confusione operativa.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 11:16:43 +02:00

210 lines
14 KiB
SQL

-- ============================================================================
-- Migration 032 - Modulo Questionari Fornitori (supply chain Art.21.2.d)
-- ----------------------------------------------------------------------------
-- APPLICATA su produzione (host MySQL) il 2026-05-31. Idempotente: ri-eseguibile.
--
-- 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;