- 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>
210 lines
14 KiB
SQL
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;
|