[FIX] E2E testing - fix router, EmailService, frontend data mapping

Critical fixes discovered during end-to-end testing:

Router (index.php):
- Rewrote route resolution engine to properly handle /{id}/subAction patterns
- All routes like GET /assessments/{id}/questions, POST /incidents/{id}/early-warning,
  GET /organizations/{id}/members now resolve correctly
- Routes with kebab-case sub-actions (early-warning, ai-analyze) now convert to camelCase
- Controller methods receive correct arguments via spread operator

EmailService.php:
- Fix PHP parse error: ?? operator cannot be used inside string interpolation {}
- Extract incident_code to variable before interpolation (3 occurrences)

assessment.html:
- Fix data structure handling: API returns categories with nested questions array
- Fix field names: question_code (not question_id), response_value (not compliance_level)
- Fix answer enum values: not_implemented/partial/implemented (not Italian)
- Fix question text field: question_text (not text/question/title)
- Show NIS2 article and ISO 27001 control references
- Fix response restoration from existing answers

dashboard.html:
- Fix data mapping from overview API response structure
- risks.total instead of open_risks, policies array instead of approved_policies
- Calculate training completion percentage from training object
- Load deadlines/activity from dedicated endpoints (not included in overview)

onboarding.html:
- Fix field name mismatches: annual_turnover_eur, contact_email, contact_phone,
  full_name, phone (matching OnboardingController expected params)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-17 19:40:26 +01:00
co-authored by Claude Opus 4.6
parent 6f4b457ce0
commit bcc5a2b003
6 changed files with 262 additions and 318 deletions
+64 -53
View File
@@ -283,60 +283,79 @@ $actionMap = [
$actions = $actionMap[$controllerName] ?? [];
$resolvedAction = null;
$callArgs = [];
// Costruisci combinazioni di pattern da verificare (ordine di specificità)
$patterns = [];
// Helper: convert kebab-case to camelCase (same logic as $actionName conversion)
$toCamel = function (string $s): string {
return str_replace('-', '', lcfirst(ucwords($s, '-')));
};
if ($subResourceId !== null && $subAction !== null) {
// METHOD:action/{id}/subAction/{subId}
$patterns[] = "{$method}:{$actionName}/{$subAction}/{subId}";
// METHOD:{id}/subAction/{subId}
$patterns[] = "{$method}:{id}/{$subAction}/{subId}";
}
// Costruisci candidati pattern → argomenti (ordine: più specifico prima)
$candidates = [];
if ($subAction !== null && $resourceId !== null) {
// METHOD:{id}/subAction
$patterns[] = "{$method}:{id}/{$subAction}";
// METHOD:action/{subId}
$patterns[] = "{$method}:{$actionName}/{subId}";
}
if (is_numeric($actionName)) {
// Il primo segmento è un ID numerico:
// /controller/123 → METHOD:{id}
// /controller/123/sub → METHOD:{id}/sub
// /controller/123/sub/456 → METHOD:{id}/sub/{subId}
$numericId = (int) $actionName;
$sub = $resourceId !== null ? $toCamel($resourceId) : null;
$subId = $subAction !== null && is_numeric($subAction) ? (int) $subAction : null;
if ($resourceId !== null && $subAction === null) {
// METHOD:action/{id} (actionName è in realtà l'ID numerico)
if (is_numeric($actionName)) {
$patterns[] = "{$method}:{id}";
$resourceId = (int) $actionName;
} else {
// METHOD:{id}
$patterns[] = "{$method}:{id}";
if ($sub !== null && $subId !== null) {
$candidates[] = ['p' => "{$method}:{id}/{$sub}/{subId}", 'a' => [$numericId, $subId]];
}
if ($sub !== null) {
$candidates[] = ['p' => "{$method}:{id}/{$sub}", 'a' => [$numericId]];
}
$candidates[] = ['p' => "{$method}:{id}", 'a' => [$numericId]];
} else {
// Il primo segmento è un'azione nominale:
// /controller/action → METHOD:action
// /controller/action/123 → METHOD:action/{subId} oppure METHOD:{id}
// /controller/action/sub → METHOD:action/sub
// /controller/action/123/sub → METHOD:{id}/sub
// /controller/action/123/sub/456 → METHOD:{id}/sub/{subId}
if ($subAction !== null && $resourceId !== null && is_numeric($resourceId)) {
$rid = (int) $resourceId;
$camelSub = $toCamel($subAction);
if ($subResourceId !== null && is_numeric($subResourceId)) {
$sid = (int) $subResourceId;
$candidates[] = ['p' => "{$method}:{id}/{$camelSub}/{subId}", 'a' => [$rid, $sid]];
}
$candidates[] = ['p' => "{$method}:{id}/{$camelSub}", 'a' => [$rid]];
$candidates[] = ['p' => "{$method}:{$actionName}/{subId}", 'a' => [$rid]];
}
if ($resourceId !== null && $subAction === null) {
if (is_numeric($resourceId)) {
// /controller/action/123 → potrebbe essere action/{subId} o {id}
$rid = (int) $resourceId;
$candidates[] = ['p' => "{$method}:{$actionName}/{subId}", 'a' => [$rid]];
$candidates[] = ['p' => "{$method}:{id}", 'a' => [$rid]];
} else {
// /controller/action/subAction → nome composto (es: evidence/upload)
$camelResource = $toCamel($resourceId);
$candidates[] = ['p' => "{$method}:{$actionName}/{$camelResource}", 'a' => []];
}
}
// /controller/action
$candidates[] = ['p' => "{$method}:{$actionName}", 'a' => []];
}
// METHOD:action/subAction
if ($resourceId !== null && !is_numeric($actionName)) {
$patterns[] = "{$method}:{$actionName}/{$resourceId}";
}
// METHOD:action
$patterns[] = "{$method}:{$actionName}";
// Cerca match
foreach ($patterns as $pattern) {
if (isset($actions[$pattern])) {
$resolvedAction = $actions[$pattern];
// Cerca primo match
foreach ($candidates as $candidate) {
if (isset($actions[$candidate['p']])) {
$resolvedAction = $actions[$candidate['p']];
$callArgs = $candidate['a'];
break;
}
}
// Se il primo segmento è numerico, l'azione è basata sull'ID
if (!$resolvedAction && is_numeric($actionName)) {
$resourceId = (int) $actionName;
$pattern = "{$method}:{id}";
if (isset($actions[$pattern])) {
$resolvedAction = $actions[$pattern];
}
}
if (!$resolvedAction) {
http_response_code(404);
header('Content-Type: application/json');
@@ -365,16 +384,8 @@ try {
exit;
}
// Chiama con gli argomenti appropriati
if ($subResourceId !== null) {
$controller->$resolvedAction((int) $resourceId, (int) $subResourceId);
} elseif ($resourceId !== null && !is_numeric($actionName)) {
$controller->$resolvedAction((int) $resourceId);
} elseif (is_numeric($actionName)) {
$controller->$resolvedAction((int) $resourceId);
} else {
$controller->$resolvedAction();
}
// Chiama con gli argomenti risolti
$controller->$resolvedAction(...$callArgs);
} catch (RuntimeException $e) {
// Rate limit exceeded (429)
if ($e->getCode() === 429) {