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) ); Database::update('organizations', [ 'entity_type' => $entityType, ], '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, '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', ]); } } }