1. Fix auto-fill visura: mapping corretto suggested_sector e employees_range, indicatori visivi verdi sui campi auto-compilati, fatturato sempre manuale 2. Adesione volontaria: colonna voluntary_compliance, checkbox in onboarding step 5 quando not_applicable, toggle in settings, reset su ri-classificazione 3. Modulo NCR/CAPA: NonConformityController con 10 endpoint API, tabelle non_conformities + capa_actions, generazione NCR dai gap assessment, predisposizione integrazione SistemiG.agile (webhook + sync) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
338 lines
14 KiB
PHP
338 lines
14 KiB
PHP
<?php
|
|
/**
|
|
* NIS2 Agile - Onboarding Controller
|
|
*
|
|
* Gestisce il wizard di onboarding: upload visura, fetch CertiSource, completamento.
|
|
*/
|
|
|
|
require_once __DIR__ . '/BaseController.php';
|
|
require_once __DIR__ . '/../services/VisuraService.php';
|
|
|
|
class OnboardingController extends BaseController
|
|
{
|
|
/**
|
|
* POST /api/onboarding/upload-visura
|
|
* Riceve PDF visura camerale, estrae dati con AI
|
|
*/
|
|
public function uploadVisura(): void
|
|
{
|
|
$this->requireAuth();
|
|
|
|
// Validate file upload
|
|
if (!isset($_FILES['visura']) || $_FILES['visura']['error'] !== UPLOAD_ERR_OK) {
|
|
$this->jsonError('Nessun file caricato o errore upload', 400, 'UPLOAD_ERROR');
|
|
}
|
|
|
|
$file = $_FILES['visura'];
|
|
|
|
// Validate file type (PDF only)
|
|
$allowedTypes = ['application/pdf'];
|
|
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
|
$mimeType = finfo_file($finfo, $file['tmp_name']);
|
|
finfo_close($finfo);
|
|
|
|
if (!in_array($mimeType, $allowedTypes)) {
|
|
$this->jsonError('Solo file PDF sono accettati', 400, 'INVALID_FILE_TYPE');
|
|
}
|
|
|
|
// Max 10MB
|
|
if ($file['size'] > 10 * 1024 * 1024) {
|
|
$this->jsonError('File troppo grande (max 10MB)', 400, 'FILE_TOO_LARGE');
|
|
}
|
|
|
|
// Save file temporarily
|
|
$uploadDir = UPLOAD_PATH . '/visure';
|
|
if (!is_dir($uploadDir)) {
|
|
mkdir($uploadDir, 0755, true);
|
|
}
|
|
|
|
$fileName = 'visura_' . $this->getCurrentUserId() . '_' . time() . '.pdf';
|
|
$filePath = $uploadDir . '/' . $fileName;
|
|
|
|
if (!move_uploaded_file($file['tmp_name'], $filePath)) {
|
|
$this->jsonError('Errore nel salvataggio del file', 500, 'SAVE_ERROR');
|
|
}
|
|
|
|
try {
|
|
// Extract data using AI
|
|
$visuraService = new VisuraService();
|
|
$extractedData = $visuraService->extractFromPdf($filePath);
|
|
|
|
$this->logAudit('visura_uploaded', 'onboarding', null, [
|
|
'file' => $fileName,
|
|
'extracted' => !empty($extractedData)
|
|
]);
|
|
|
|
$this->jsonSuccess($extractedData, 'Dati estratti dalla visura');
|
|
} catch (Throwable $e) {
|
|
error_log('[VISURA_ERROR] ' . $e->getMessage());
|
|
$this->jsonError(
|
|
APP_DEBUG ? 'Errore estrazione: ' . $e->getMessage() : 'Errore nell\'analisi della visura',
|
|
500,
|
|
'EXTRACTION_ERROR'
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/onboarding/fetch-company
|
|
* Recupera dati aziendali da CertiSource tramite P.IVA
|
|
*/
|
|
public function fetchCompany(): void
|
|
{
|
|
$this->requireAuth();
|
|
$this->validateRequired(['vat_number']);
|
|
|
|
$vatNumber = trim($this->getParam('vat_number'));
|
|
|
|
// Clean VAT: remove IT prefix, spaces
|
|
$vatNumber = preg_replace('/^IT/i', '', $vatNumber);
|
|
$vatNumber = preg_replace('/\s+/', '', $vatNumber);
|
|
|
|
// Validate Italian P.IVA format (11 digits)
|
|
if (!preg_match('/^\d{11}$/', $vatNumber)) {
|
|
$this->jsonError('Formato Partita IVA non valido (11 cifre)', 400, 'INVALID_VAT');
|
|
}
|
|
|
|
try {
|
|
$visuraService = new VisuraService();
|
|
$companyData = $visuraService->fetchFromCertiSource($vatNumber);
|
|
|
|
$this->logAudit('certisource_fetch', 'onboarding', null, [
|
|
'vat_number' => $vatNumber,
|
|
'found' => !empty($companyData)
|
|
]);
|
|
|
|
if (empty($companyData) || empty($companyData['company_name'])) {
|
|
$this->jsonError(
|
|
'Azienda non trovata per la P.IVA fornita',
|
|
404,
|
|
'COMPANY_NOT_FOUND'
|
|
);
|
|
}
|
|
|
|
$this->jsonSuccess($companyData, 'Dati aziendali recuperati');
|
|
} catch (Throwable $e) {
|
|
error_log('[CERTISOURCE_ERROR] ' . $e->getMessage());
|
|
$this->jsonError(
|
|
APP_DEBUG ? 'Errore CertiSource: ' . $e->getMessage() : 'Errore nel recupero dati aziendali',
|
|
500,
|
|
'CERTISOURCE_ERROR'
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/onboarding/complete
|
|
* Completa l'onboarding: crea organizzazione e aggiorna profilo utente
|
|
*/
|
|
public function complete(): void
|
|
{
|
|
$this->requireAuth();
|
|
$this->validateRequired(['name', 'sector']);
|
|
|
|
$userId = $this->getCurrentUserId();
|
|
|
|
// Check if user already has an organization
|
|
$existingOrg = Database::fetchOne(
|
|
'SELECT organization_id FROM user_organizations WHERE user_id = ? AND is_primary = 1',
|
|
[$userId]
|
|
);
|
|
|
|
if ($existingOrg) {
|
|
$this->jsonError('Hai già un\'organizzazione configurata', 409, 'ORG_EXISTS');
|
|
}
|
|
|
|
Database::beginTransaction();
|
|
try {
|
|
// Create organization
|
|
$orgData = [
|
|
'name' => trim($this->getParam('name')),
|
|
'vat_number' => $this->getParam('vat_number'),
|
|
'fiscal_code' => $this->getParam('fiscal_code'),
|
|
'sector' => $this->getParam('sector'),
|
|
'employee_count' => $this->getParam('employee_count'),
|
|
'annual_turnover_eur' => $this->getParam('annual_turnover_eur'),
|
|
'country' => $this->getParam('country', 'IT'),
|
|
'city' => $this->getParam('city'),
|
|
'address' => $this->getParam('address'),
|
|
'website' => $this->getParam('website'),
|
|
'contact_email' => $this->getParam('contact_email'),
|
|
'contact_phone' => $this->getParam('contact_phone'),
|
|
];
|
|
|
|
$orgId = Database::insert('organizations', $orgData);
|
|
|
|
// Auto-classify NIS2 entity type
|
|
$entityType = $this->classifyNis2Entity(
|
|
$orgData['sector'],
|
|
(int) ($orgData['employee_count'] ?? 0),
|
|
(float) ($orgData['annual_turnover_eur'] ?? 0)
|
|
);
|
|
|
|
// Handle voluntary compliance
|
|
$voluntaryCompliance = ($entityType === 'not_applicable' && (int) $this->getParam('voluntary_compliance', 0)) ? 1 : 0;
|
|
|
|
Database::update('organizations', [
|
|
'entity_type' => $entityType,
|
|
'voluntary_compliance' => $voluntaryCompliance,
|
|
], 'id = ?', [$orgId]);
|
|
|
|
// Link user as org_admin
|
|
Database::insert('user_organizations', [
|
|
'user_id' => $userId,
|
|
'organization_id' => $orgId,
|
|
'role' => 'org_admin',
|
|
'is_primary' => 1,
|
|
]);
|
|
|
|
// Update user profile if provided
|
|
$profileUpdates = [];
|
|
if ($this->hasParam('phone') && $this->getParam('phone')) {
|
|
$profileUpdates['phone'] = $this->getParam('phone');
|
|
}
|
|
if ($this->hasParam('full_name') && $this->getParam('full_name')) {
|
|
$profileUpdates['full_name'] = trim($this->getParam('full_name'));
|
|
}
|
|
if (!empty($profileUpdates)) {
|
|
Database::update('users', $profileUpdates, 'id = ?', [$userId]);
|
|
}
|
|
|
|
// Initialize NIS2 compliance controls
|
|
$this->initializeComplianceControls($orgId);
|
|
|
|
Database::commit();
|
|
|
|
$this->currentOrgId = $orgId;
|
|
$this->logAudit('onboarding_completed', 'organization', $orgId, [
|
|
'name' => $orgData['name'],
|
|
'sector' => $orgData['sector'],
|
|
'entity_type' => $entityType
|
|
]);
|
|
|
|
$this->jsonSuccess([
|
|
'organization_id' => $orgId,
|
|
'name' => $orgData['name'],
|
|
'entity_type' => $entityType,
|
|
'voluntary_compliance' => $voluntaryCompliance,
|
|
'classification' => $this->getClassificationDetails($entityType, $orgData['sector'], (int)($orgData['employee_count'] ?? 0), (float)($orgData['annual_turnover_eur'] ?? 0)),
|
|
], 'Onboarding completato', 201);
|
|
|
|
} catch (Throwable $e) {
|
|
Database::rollback();
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// PRIVATE METHODS (copied from OrganizationController for independence)
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
private function classifyNis2Entity(string $sector, int $employees, float $turnover): string
|
|
{
|
|
// Map detailed sector codes to NIS2 categories
|
|
$essentialSectors = [
|
|
'energy', 'energy_electricity', 'energy_district_heating', 'energy_oil', 'energy_gas', 'energy_hydrogen',
|
|
'transport', 'transport_air', 'transport_rail', 'transport_water', 'transport_road',
|
|
'banking', 'financial_markets', 'health', 'water', 'drinking_water', 'waste_water',
|
|
'digital_infra', 'digital_infrastructure', 'ict_service_management',
|
|
'public_admin', 'public_administration', 'space',
|
|
];
|
|
|
|
$importantSectors = [
|
|
'manufacturing', 'manufacturing_medical', 'manufacturing_computers', 'manufacturing_electrical',
|
|
'manufacturing_machinery', 'manufacturing_vehicles', 'manufacturing_transport',
|
|
'postal', 'postal_courier', 'chemical', 'chemicals', 'food',
|
|
'waste', 'waste_management', 'ict_services', 'digital_providers', 'research',
|
|
];
|
|
|
|
$isLarge = $employees >= 250 || $turnover >= 50000000;
|
|
$isMedium = ($employees >= 50 || $turnover >= 10000000) && !$isLarge;
|
|
|
|
if (in_array($sector, $essentialSectors)) {
|
|
if ($isLarge) return 'essential';
|
|
if ($isMedium) return 'important';
|
|
}
|
|
|
|
if (in_array($sector, $importantSectors)) {
|
|
if ($isLarge || $isMedium) return 'important';
|
|
}
|
|
|
|
return 'not_applicable';
|
|
}
|
|
|
|
private function getClassificationDetails(string $type, string $sector, int $employees, float $turnover): array
|
|
{
|
|
$sizeLabel = $employees >= 250 ? 'Grande Impresa' : ($employees >= 50 ? 'Media Impresa' : 'Piccola Impresa');
|
|
|
|
return match ($type) {
|
|
'essential' => [
|
|
'type' => 'essential',
|
|
'label' => 'Soggetto Essenziale',
|
|
'size' => $sizeLabel,
|
|
'description' => "La vostra organizzazione rientra tra i soggetti essenziali ai sensi della Direttiva NIS2 (UE 2022/2555). Operate in un settore ad alta criticità e superate le soglie dimensionali.",
|
|
'obligations' => [
|
|
'Misure di sicurezza informatica (Art. 21)',
|
|
'Notifica incidenti significativi al CSIRT entro 24h/72h/30gg (Art. 23)',
|
|
'Formazione obbligatoria per gli organi di gestione (Art. 20)',
|
|
'Vigilanza proattiva (ex ante) da parte delle autorità',
|
|
'Sanzioni fino a EUR 10M o 2% del fatturato mondiale annuo',
|
|
],
|
|
],
|
|
'important' => [
|
|
'type' => 'important',
|
|
'label' => 'Soggetto Importante',
|
|
'size' => $sizeLabel,
|
|
'description' => "La vostra organizzazione rientra tra i soggetti importanti ai sensi della Direttiva NIS2. Siete tenuti a rispettare gli obblighi di sicurezza e notifica incidenti.",
|
|
'obligations' => [
|
|
'Misure di sicurezza informatica (Art. 21)',
|
|
'Notifica incidenti significativi al CSIRT entro 24h/72h/30gg (Art. 23)',
|
|
'Formazione obbligatoria per gli organi di gestione (Art. 20)',
|
|
'Vigilanza reattiva (ex post) da parte delle autorità',
|
|
'Sanzioni fino a EUR 7M o 1,4% del fatturato mondiale annuo',
|
|
],
|
|
],
|
|
default => [
|
|
'type' => 'not_applicable',
|
|
'label' => 'Non Applicabile',
|
|
'size' => $sizeLabel,
|
|
'description' => "In base ai dati forniti, la vostra organizzazione non sembra rientrare nell'ambito di applicazione della Direttiva NIS2. Consigliamo comunque di adottare le best practice di cybersecurity.",
|
|
'obligations' => [
|
|
'Nessun obbligo NIS2 specifico',
|
|
'Si raccomandano comunque buone pratiche di sicurezza informatica',
|
|
'Possibile designazione futura da parte delle autorità nazionali',
|
|
],
|
|
],
|
|
};
|
|
}
|
|
|
|
private function initializeComplianceControls(int $orgId): void
|
|
{
|
|
$controls = [
|
|
['NIS2-21.2.a', 'nis2', 'Politiche di analisi dei rischi e sicurezza dei sistemi informatici'],
|
|
['NIS2-21.2.b', 'nis2', 'Gestione degli incidenti'],
|
|
['NIS2-21.2.c', 'nis2', 'Continuità operativa e gestione delle crisi'],
|
|
['NIS2-21.2.d', 'nis2', 'Sicurezza della catena di approvvigionamento'],
|
|
['NIS2-21.2.e', 'nis2', 'Sicurezza acquisizione, sviluppo e manutenzione sistemi'],
|
|
['NIS2-21.2.f', 'nis2', 'Politiche e procedure per valutare efficacia misure'],
|
|
['NIS2-21.2.g', 'nis2', 'Pratiche di igiene informatica di base e formazione'],
|
|
['NIS2-21.2.h', 'nis2', 'Politiche e procedure relative alla crittografia'],
|
|
['NIS2-21.2.i', 'nis2', 'Sicurezza risorse umane, controllo accessi e gestione asset'],
|
|
['NIS2-21.2.j', 'nis2', 'Autenticazione multi-fattore e comunicazioni sicure'],
|
|
['NIS2-20.1', 'nis2', 'Governance: approvazione misure da parte degli organi di gestione'],
|
|
['NIS2-20.2', 'nis2', 'Formazione obbligatoria per gli organi di gestione'],
|
|
['NIS2-23.1', 'nis2', 'Notifica incidenti significativi al CSIRT (24h/72h/30gg)'],
|
|
];
|
|
|
|
foreach ($controls as [$code, $framework, $title]) {
|
|
Database::insert('compliance_controls', [
|
|
'organization_id' => $orgId,
|
|
'control_code' => $code,
|
|
'framework' => $framework,
|
|
'title' => $title,
|
|
'status' => 'not_started',
|
|
]);
|
|
}
|
|
}
|
|
}
|