[FEAT] A2 backend: Nis2ClassificationService v2 (matrice+bilancio, size-independent art.3 c.5, criteri sotto-soglia c.9 b/c/d, obblighi D.Lgs. art.23/24/25, esito 'preliminare non vincolante'); logica centralizzata (no duplicazione) + complete() salva i nuovi campi

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-06-15 17:06:17 +02:00
co-authored by Claude Opus 4.8
parent b2f47c0a47
commit c5fce7bdf7
3 changed files with 208 additions and 76 deletions
@@ -8,6 +8,7 @@
require_once __DIR__ . '/BaseController.php';
require_once __DIR__ . '/../services/VisuraService.php';
require_once APP_PATH . '/services/RateLimitService.php';
require_once __DIR__ . '/../services/Nis2ClassificationService.php';
class OnboardingController extends BaseController
{
@@ -217,6 +218,15 @@ class OnboardingController extends BaseController
'sector' => $this->getParam('sector'),
'employee_count' => $this->getParam('employee_count'),
'annual_turnover_eur' => $this->getParam('annual_turnover_eur'),
'total_balance_eur' => $this->getParam('total_balance_eur'),
'is_autonomous' => (int) $this->getParam('is_autonomous', 1),
'size_independent_category' => $this->getParam('size_independent_category', 'none'),
'crit_sole_supplier' => $this->hasParam('crit_sole_supplier') ? (int) $this->getParam('crit_sole_supplier') : null,
'crit_public_impact' => $this->hasParam('crit_public_impact') ? (int) $this->getParam('crit_public_impact') : null,
'crit_systemic_risk' => $this->hasParam('crit_systemic_risk') ? (int) $this->getParam('crit_systemic_risk') : null,
'mission' => $this->getParam('mission'),
'has_code_of_ethics' => (int) $this->getParam('has_code_of_ethics', 0),
'code_of_ethics_note' => $this->getParam('code_of_ethics_note'),
'country' => $this->getParam('country', 'IT'),
'city' => $this->getParam('city'),
'address' => $this->getParam('address'),
@@ -227,19 +237,28 @@ class OnboardingController extends BaseController
$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)
);
// Auto-classify NIS2 (AUTOVALUTAZIONE PRELIMINARE) — logica centralizzata v2
$cls = Nis2ClassificationService::classify([
'sector' => $orgData['sector'],
'employees' => (int) ($orgData['employee_count'] ?? 0),
'turnover' => (float) ($orgData['annual_turnover_eur'] ?? 0),
'balance' => (float) ($orgData['total_balance_eur'] ?? 0),
'size_independent_category' => $orgData['size_independent_category'],
'is_autonomous' => $orgData['is_autonomous'],
'crit_sole_supplier' => $orgData['crit_sole_supplier'],
'crit_public_impact' => $orgData['crit_public_impact'],
'crit_systemic_risk' => $orgData['crit_systemic_risk'],
]);
$entityType = $cls['entity_type'];
// Handle voluntary compliance
// Handle voluntary compliance (solo per soggetti non in ambito)
$voluntaryCompliance = ($entityType === 'not_applicable' && (int) $this->getParam('voluntary_compliance', 0)) ? 1 : 0;
Database::update('organizations', [
'entity_type' => $entityType,
'voluntary_compliance' => $voluntaryCompliance,
'entity_type' => $entityType,
'voluntary_compliance' => $voluntaryCompliance,
'designation_basis' => $cls['designation_basis'],
'self_assessment_status' => $cls['self_assessment_status'],
], 'id = ?', [$orgId]);
// Link user: consulente → ruolo 'consultant', is_primary solo se prima org
@@ -282,7 +301,7 @@ class OnboardingController extends BaseController
'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)),
'classification' => $cls,
], 'Onboarding completato', 201);
} catch (Throwable $e) {
@@ -297,35 +316,8 @@ class OnboardingController extends BaseController
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';
// Logica centralizzata in Nis2ClassificationService (A2).
return Nis2ClassificationService::entityType($sector, $employees, $turnover);
}
private function getClassificationDetails(string $type, string $sector, int $employees, float $turnover): array