227 lines
7.2 KiB
PHP
227 lines
7.2 KiB
PHP
<?php
|
|
|
|
require_once __DIR__ . '/questionnaire_structure.php';
|
|
|
|
/**
|
|
* Validate Android app questionnaire submit payloads before persisting answers.
|
|
*
|
|
* @param array<string, mixed>|null $manifest Structure manifest for legacy revision submits
|
|
* @return list<array{questionID: string, code: string, message: string}>
|
|
*/
|
|
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;
|
|
}
|
|
|
|
$type = $shortIdToType[$shortId] ?? '';
|
|
if (qdb_is_non_stored_answer_question_type($type)) {
|
|
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<array{questionID: string, code: string, message: string}>
|
|
*/
|
|
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;
|
|
}
|