|null $manifest Structure manifest for legacy revision submits * @return list */ function qdb_validate_app_submit_payload( PDO $pdo, string $qnID, array $rawAnswers, array $shortIdMap, array $shortIdToType, array $symptomParentMap, array $optionMap, ?array $manifest = null ): array { $errors = []; if ($rawAnswers === []) { $errors[] = [ 'questionID' => '', 'code' => 'ANSWERS_REQUIRED', 'message' => 'At least one answer is required', ]; return $errors; } foreach ($rawAnswers as $idx => $a) { if (!is_array($a)) { $errors[] = [ 'questionID' => '', 'code' => 'INVALID_ANSWER', 'message' => 'Answer at index ' . $idx . ' must be an object', ]; continue; } $shortId = trim($a['questionID'] ?? ''); if ($shortId === '') { $errors[] = [ 'questionID' => '', 'code' => 'MISSING_QUESTION_ID', 'message' => 'Each answer must include questionID', ]; continue; } if (isset($symptomParentMap[$shortId])) { $label = trim((string)($a['answerOptionKey'] ?? $a['freeTextValue'] ?? '')); if ($label === '') { $errors[] = [ 'questionID' => $shortId, 'code' => 'EMPTY_ANSWER', 'message' => 'Symptom selection is required', ]; } continue; } if (!isset($shortIdMap[$shortId])) { $errors[] = [ 'questionID' => $shortId, 'code' => 'UNKNOWN_QUESTION', 'message' => 'Unknown question: ' . $shortId, ]; continue; } $fullQID = $shortIdMap[$shortId]; $type = $shortIdToType[$shortId] ?? ''; $optKey = $a['answerOptionKey'] ?? null; $free = isset($a['freeTextValue']) ? trim((string)$a['freeTextValue']) : ''; $numeric = $a['numericValue'] ?? null; $hasNumeric = $numeric !== null && $numeric !== ''; if ($type === 'multi_check_box_question') { if ($optKey !== null && trim((string)$optKey) !== '') { continue; } if ($free !== '') { continue; } $errors[] = [ 'questionID' => $shortId, 'code' => 'EMPTY_ANSWER', 'message' => 'Select at least one option', ]; continue; } if ($optKey !== null && trim((string)$optKey) !== '') { $key = (string)$optKey; if (!isset($optionMap[$fullQID][$key])) { $errors[] = [ 'questionID' => $shortId, 'code' => 'INVALID_OPTION', 'message' => 'Unknown option key: ' . $key, ]; } continue; } if ($free !== '' || $hasNumeric) { continue; } $errors[] = [ 'questionID' => $shortId, 'code' => 'EMPTY_ANSWER', 'message' => 'Answer is empty', ]; } if ($errors !== []) { return $errors; } require_once __DIR__ . '/app_answers.php'; $grouped = qdb_group_app_submit_answers($rawAnswers, $shortIdToType, $symptomParentMap); $answeredShort = []; foreach ($grouped as $row) { $sid = trim($row['questionID'] ?? ''); if ($sid !== '') { $answeredShort[$sid] = true; } } if ($manifest !== null) { return array_merge($errors, qdb_validate_required_answers_from_manifest($manifest, $answeredShort)); } $qStmt = $pdo->prepare( 'SELECT questionID, type, isRequired, configJson FROM question WHERE questionnaireID = :qn AND ' . qdb_active_questions_clause('question') ); $qStmt->execute([':qn' => $qnID]); foreach ($qStmt->fetchAll(PDO::FETCH_ASSOC) as $qRow) { if ((int)($qRow['isRequired'] ?? 0) !== 1) { continue; } $fullId = $qRow['questionID']; $parts = explode('__', $fullId); $short = end($parts); $type = $qRow['type'] ?? ''; if ($type === 'glass_scale_question') { $cfg = json_decode($qRow['configJson'] ?? '{}', true) ?: []; $symptoms = $cfg['symptoms'] ?? []; if (is_array($symptoms) && $symptoms !== []) { foreach ($symptoms as $symptomKey) { $sk = trim((string)$symptomKey); if ($sk === '') { continue; } if (empty($answeredShort[$sk])) { $errors[] = [ 'questionID' => $sk, 'code' => 'MISSING_ANSWER', 'message' => 'Required symptom is not answered', ]; } } continue; } } if (empty($answeredShort[$short])) { $errors[] = [ 'questionID' => $short, 'code' => 'MISSING_ANSWER', 'message' => 'Required question is not answered', ]; } } return $errors; } /** * @return list */ function qdb_validate_required_answers_from_manifest(array $manifest, array $answeredShort): array { $errors = []; foreach ($manifest['questions'] ?? [] as $qEntry) { if (!is_array($qEntry) || empty($qEntry['isRequired'])) { continue; } $shortId = (string)($qEntry['shortId'] ?? ''); $type = (string)($qEntry['type'] ?? ''); if ($shortId === '') { continue; } if ($type === 'glass_scale_question') { foreach ($qEntry['symptoms'] ?? [] as $symptomKey) { $sk = trim((string)$symptomKey); if ($sk === '') { continue; } if (empty($answeredShort[$sk])) { $errors[] = [ 'questionID' => $sk, 'code' => 'MISSING_ANSWER', 'message' => 'Required symptom is not answered', ]; } } continue; } if (empty($answeredShort[$shortId])) { $errors[] = [ 'questionID' => $shortId, 'code' => 'MISSING_ANSWER', 'message' => 'Required question is not answered', ]; } } return $errors; }