[FEAT] Gap Analysis estesa ai requisiti ACN (specifiche di base 164179/2025)
Allinea il PRODOTTO alla guida/normativa portando la compliance dal livello 10 misure Art.21 al livello operativo dei requisiti ACN (Framework Nazionale 2025). - Migrazione 031: acn_requirements (catalogo) + org_acn_requirement_status (stato per-org) - Seed da Allegati 1-2 ACN (fonte certa, parsing verificato): 87 importanti + 116 essenziali = 203 requisiti reali - AuditController: acnRequirements (GET, per entity_type org: importanti 87 / essenziali 116, summary per funzione GV/ID/PR/DE/RS/RC, % compliance) + updateAcnRequirement (PUT stato+evidenza) - Route audit/acnRequirements GET/PUT - guida.html: fix refuso cap-5 (residuo 'otto categorie...no' -> '10 categorie x 8, quattro modalita') E2E prod: org importante -> 87 req; PUT implemented -> compliance aggiornata. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
6671d51969
commit
59205d05fb
@@ -470,4 +470,98 @@ class AuditController extends BaseController
|
||||
'recent_evidence' => $recent,
|
||||
]);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
// REQUISITI ACN (specifiche di base, Determina 164179/2025)
|
||||
// Gap Analysis a livello requisiti: 87 (importanti) / 116 (essenziali).
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
/** Mappa entity_type org (essential/important/...) -> entity catalogo ACN. */
|
||||
private function acnEntityFor(int $orgId): string
|
||||
{
|
||||
$org = Database::fetchOne('SELECT entity_type FROM organizations WHERE id = ?', [$orgId]);
|
||||
// I soggetti essenziali hanno il set esteso (116). Tutti gli altri (importanti o
|
||||
// non classificati) usano il set base importanti (87).
|
||||
return ($org && ($org['entity_type'] ?? '') === 'essential') ? 'essenziale' : 'importante';
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/audit/acnRequirements
|
||||
* Requisiti ACN applicabili all'org (per entity_type) con stato + sommario per funzione.
|
||||
*/
|
||||
public function acnRequirements(): void
|
||||
{
|
||||
$this->requireOrgAccess();
|
||||
$orgId = $this->getCurrentOrgId();
|
||||
$entity = $this->acnEntityFor($orgId);
|
||||
|
||||
$rows = Database::fetchAll(
|
||||
"SELECT r.id, r.function_name, r.subcategory, r.subcategory_text, r.req_index, r.requirement,
|
||||
COALESCE(s.status,'not_started') AS status, s.evidence_note, s.updated_at
|
||||
FROM acn_requirements r
|
||||
LEFT JOIN org_acn_requirement_status s
|
||||
ON s.requirement_id = r.id AND s.organization_id = ?
|
||||
WHERE r.entity = ?
|
||||
ORDER BY r.subcategory, r.req_index",
|
||||
[$orgId, $entity]
|
||||
);
|
||||
|
||||
$summary = ['not_started'=>0,'in_progress'=>0,'implemented'=>0,'not_applicable'=>0];
|
||||
$byFunc = [];
|
||||
foreach ($rows as $r) {
|
||||
$st = $r['status'];
|
||||
if (isset($summary[$st])) $summary[$st]++;
|
||||
$f = $r['function_name'];
|
||||
if (!isset($byFunc[$f])) $byFunc[$f] = ['total'=>0,'implemented'=>0];
|
||||
$byFunc[$f]['total']++;
|
||||
if ($st === 'implemented') $byFunc[$f]['implemented']++;
|
||||
}
|
||||
$total = count($rows);
|
||||
$applicable = $total - $summary['not_applicable'];
|
||||
$compliance = $applicable > 0 ? (int) round($summary['implemented'] * 100 / $applicable) : 0;
|
||||
|
||||
$this->jsonSuccess([
|
||||
'entity' => $entity,
|
||||
'total' => $total,
|
||||
'summary' => $summary,
|
||||
'by_function' => $byFunc,
|
||||
'compliance_percent'=> $compliance,
|
||||
'requirements' => $rows,
|
||||
'source' => 'Determina ACN 164179/2025, Allegati 1-2 (Framework Nazionale 2025)',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /api/audit/acnRequirements/{id}
|
||||
* Aggiorna lo stato di un requisito ACN per l'org. Body: { status, evidence_note? }
|
||||
*/
|
||||
public function updateAcnRequirement(int $id): void
|
||||
{
|
||||
$this->requireOrgRole(['org_admin', 'compliance_manager', 'auditor']);
|
||||
$orgId = $this->getCurrentOrgId();
|
||||
$entity = $this->acnEntityFor($orgId);
|
||||
|
||||
// Il requisito deve appartenere al set applicabile all'org
|
||||
$req = Database::fetchOne('SELECT id FROM acn_requirements WHERE id = ? AND entity = ?', [$id, $entity]);
|
||||
if (!$req) {
|
||||
$this->jsonError('Requisito non trovato o non applicabile a questa organizzazione', 404, 'NOT_FOUND');
|
||||
}
|
||||
|
||||
$status = $this->getParam('status');
|
||||
$valid = ['not_started','in_progress','implemented','not_applicable'];
|
||||
if (!in_array($status, $valid, true)) {
|
||||
$this->jsonError('Stato non valido', 422, 'INVALID_STATUS');
|
||||
}
|
||||
$note = $this->getParam('evidence_note');
|
||||
|
||||
Database::query(
|
||||
'INSERT INTO org_acn_requirement_status (organization_id, requirement_id, status, evidence_note, updated_by)
|
||||
VALUES (?,?,?,?,?)
|
||||
ON DUPLICATE KEY UPDATE status=VALUES(status), evidence_note=VALUES(evidence_note),
|
||||
updated_by=VALUES(updated_by), updated_at=NOW()',
|
||||
[$orgId, $id, $status, $note, $this->getCurrentUserId()]
|
||||
);
|
||||
$this->logAudit('acn_requirement_updated', 'acn_requirement', $id, ['status' => $status]);
|
||||
$this->jsonSuccess(['id' => $id, 'status' => $status], 'Requisito aggiornato');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user