enhanced submission handling with deccision trees
Some checks failed
PHPUnit / test (push) Has been cancelled
Some checks failed
PHPUnit / test (push) Has been cancelled
This commit is contained in:
@ -128,6 +128,7 @@ if ($method === 'POST') {
|
||||
|
||||
$sumPoints = 0;
|
||||
$glassByParent = [];
|
||||
$submittedQuestionIds = [];
|
||||
$answerInsert = $pdo->prepare("
|
||||
INSERT INTO client_answer (clientCode, questionID, answerOptionID, freeTextValue, numericValue, answeredAt)
|
||||
VALUES (:cc, :qid, :aoid, :ftv, :nv, :at)
|
||||
@ -137,6 +138,9 @@ if ($method === 'POST') {
|
||||
numericValue = excluded.numericValue,
|
||||
answeredAt = excluded.answeredAt
|
||||
");
|
||||
$answerDelete = $pdo->prepare(
|
||||
'DELETE FROM client_answer WHERE clientCode = :cc AND questionID = :qid'
|
||||
);
|
||||
|
||||
require_once __DIR__ . '/../lib/app_answers.php';
|
||||
$answers = qdb_group_app_submit_answers($answers, $shortIdToType, $symptomParentMap);
|
||||
@ -173,6 +177,8 @@ if ($method === 'POST') {
|
||||
$maxLen = $freeTextMaxLen[$fullQID] ?? 2000;
|
||||
$freeTextValue = sanitize_free_text($freeTextValue, $maxLen);
|
||||
if ($freeTextValue === '') {
|
||||
$answerDelete->execute([':cc' => $clientCode, ':qid' => $fullQID]);
|
||||
$submittedQuestionIds[$fullQID] = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@ -192,6 +198,15 @@ if ($method === 'POST') {
|
||||
}
|
||||
}
|
||||
|
||||
$hasValue = $answerOptionID !== null
|
||||
|| ($freeTextValue !== null && $freeTextValue !== '')
|
||||
|| $numericValue !== null;
|
||||
if (!$hasValue) {
|
||||
$answerDelete->execute([':cc' => $clientCode, ':qid' => $fullQID]);
|
||||
$submittedQuestionIds[$fullQID] = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
$answerInsert->execute([
|
||||
':cc' => $clientCode,
|
||||
':qid' => $fullQID,
|
||||
@ -200,23 +215,35 @@ if ($method === 'POST') {
|
||||
':nv' => $numericValue,
|
||||
':at' => $answeredAt,
|
||||
]);
|
||||
$submittedQuestionIds[$fullQID] = true;
|
||||
}
|
||||
|
||||
foreach ($glassByParent as $parentQID => $symptomLabels) {
|
||||
$existing = $pdo->prepare(
|
||||
'SELECT freeTextValue FROM client_answer WHERE clientCode = :cc AND questionID = :qid'
|
||||
$json = qdb_build_glass_symptom_json($symptomLabels);
|
||||
if ($json === null) {
|
||||
$answerDelete->execute([':cc' => $clientCode, ':qid' => $parentQID]);
|
||||
} else {
|
||||
$answerInsert->execute([
|
||||
':cc' => $clientCode,
|
||||
':qid' => $parentQID,
|
||||
':aoid' => null,
|
||||
':ftv' => $json,
|
||||
':nv' => null,
|
||||
':at' => $completedAt ?? $startedAt,
|
||||
]);
|
||||
}
|
||||
$submittedQuestionIds[$parentQID] = true;
|
||||
}
|
||||
|
||||
// Re-edit uploads omit answers pruned on the device (e.g. branching changes).
|
||||
// Drop live rows for questions not present in this payload.
|
||||
$staleQuestionIds = array_diff(array_values($shortIdMap), array_keys($submittedQuestionIds));
|
||||
if ($staleQuestionIds !== []) {
|
||||
$placeholders = implode(',', array_fill(0, count($staleQuestionIds), '?'));
|
||||
$staleDelete = $pdo->prepare(
|
||||
"DELETE FROM client_answer WHERE clientCode = ? AND questionID IN ($placeholders)"
|
||||
);
|
||||
$existing->execute([':cc' => $clientCode, ':qid' => $parentQID]);
|
||||
$prevJson = $existing->fetchColumn();
|
||||
$merged = qdb_merge_glass_symptom_json($prevJson !== false ? (string)$prevJson : null, $symptomLabels);
|
||||
$answerInsert->execute([
|
||||
':cc' => $clientCode,
|
||||
':qid' => $parentQID,
|
||||
':aoid' => null,
|
||||
':ftv' => $merged,
|
||||
':nv' => null,
|
||||
':at' => $completedAt ?? $startedAt,
|
||||
]);
|
||||
$staleDelete->execute(array_merge([$clientCode], array_values($staleQuestionIds)));
|
||||
}
|
||||
|
||||
// Upsert completed_questionnaire record
|
||||
|
||||
@ -48,6 +48,26 @@ function qdb_decode_multi_check_values(?string $raw): array {
|
||||
return [$trimmed];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build glass-scale symptom JSON from the current submit only (no merge with prior rows).
|
||||
*
|
||||
* @param array<string, string> $symptomLabels symptom key => selected label
|
||||
*/
|
||||
function qdb_build_glass_symptom_json(array $symptomLabels): ?string {
|
||||
$data = [];
|
||||
foreach ($symptomLabels as $key => $label) {
|
||||
$k = trim((string)$key);
|
||||
$v = trim((string)$label);
|
||||
if ($k !== '' && $v !== '') {
|
||||
$data[$k] = $v;
|
||||
}
|
||||
}
|
||||
if ($data === []) {
|
||||
return null;
|
||||
}
|
||||
return json_encode($data, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Group raw POST answer rows by question short id; merge multi_check option keys.
|
||||
*
|
||||
|
||||
@ -9,6 +9,94 @@ use Tests\Support\QdbTestCase;
|
||||
|
||||
final class AppSubmitExtendedTest extends QdbTestCase
|
||||
{
|
||||
public function testResubmitClearsOmittedOptionalAnswers(): void
|
||||
{
|
||||
$login = $this->api()->loginMobileAndGetToken(
|
||||
DatabaseSeeder::COACH_USERNAME,
|
||||
DatabaseSeeder::PASSWORD
|
||||
);
|
||||
$f = $this->fixture();
|
||||
|
||||
$this->assertApiOk($this->submitQuestionnaireAs($login['token'], $f->questionnaireId, $f->clientCode, [
|
||||
['questionID' => $f->questionShortId, 'answerOptionKey' => 'yes'],
|
||||
['questionID' => $f->freeTextShortId, 'freeTextValue' => 'Old notes'],
|
||||
['questionID' => $f->glassSymptomKey, 'freeTextValue' => 'moderate'],
|
||||
['questionID' => 'fatigue', 'freeTextValue' => 'severe'],
|
||||
]));
|
||||
|
||||
$res = $this->submitQuestionnaireAs($login['token'], $f->questionnaireId, $f->clientCode, [
|
||||
['questionID' => $f->questionShortId, 'answerOptionKey' => 'yes'],
|
||||
]);
|
||||
$this->assertApiOk($res);
|
||||
|
||||
[$pdo, $tmpDb, $lockFp] = qdb_open(false);
|
||||
|
||||
$ft = $pdo->prepare(
|
||||
'SELECT freeTextValue FROM client_answer WHERE clientCode = :cc AND questionID = :qid'
|
||||
);
|
||||
$ft->execute([':cc' => $f->clientCode, ':qid' => $f->freeTextQuestionId]);
|
||||
$this->assertFalse($ft->fetchColumn());
|
||||
|
||||
$glass = $pdo->prepare(
|
||||
'SELECT freeTextValue FROM client_answer WHERE clientCode = :cc AND questionID = :qid'
|
||||
);
|
||||
$glass->execute([':cc' => $f->clientCode, ':qid' => $f->glassQuestionId]);
|
||||
$this->assertFalse($glass->fetchColumn());
|
||||
|
||||
$sub = $pdo->prepare(
|
||||
'SELECT submissionID FROM questionnaire_submission
|
||||
WHERE clientCode = :cc AND questionnaireID = :qn ORDER BY version DESC LIMIT 1'
|
||||
);
|
||||
$sub->execute([':cc' => $f->clientCode, ':qn' => $f->questionnaireId]);
|
||||
$submissionId = (string)$sub->fetchColumn();
|
||||
|
||||
$archived = $pdo->prepare(
|
||||
'SELECT freeTextValue FROM client_answer_submission
|
||||
WHERE submissionID = :sid AND questionID = :qid'
|
||||
);
|
||||
$archived->execute([':sid' => $submissionId, ':qid' => $f->freeTextQuestionId]);
|
||||
$this->assertFalse($archived->fetchColumn());
|
||||
|
||||
$archivedGlass = $pdo->prepare(
|
||||
'SELECT freeTextValue FROM client_answer_submission
|
||||
WHERE submissionID = :sid AND questionID = :qid'
|
||||
);
|
||||
$archivedGlass->execute([':sid' => $submissionId, ':qid' => $f->glassQuestionId]);
|
||||
$this->assertFalse($archivedGlass->fetchColumn());
|
||||
|
||||
qdb_discard($tmpDb, $lockFp);
|
||||
}
|
||||
|
||||
public function testResubmitReplacesGlassSymptomsInsteadOfMerging(): void
|
||||
{
|
||||
$login = $this->api()->loginMobileAndGetToken(
|
||||
DatabaseSeeder::COACH_USERNAME,
|
||||
DatabaseSeeder::PASSWORD
|
||||
);
|
||||
$f = $this->fixture();
|
||||
|
||||
$this->assertApiOk($this->submitQuestionnaireAs($login['token'], $f->questionnaireId, $f->clientCode, [
|
||||
['questionID' => $f->questionShortId, 'answerOptionKey' => 'yes'],
|
||||
['questionID' => $f->glassSymptomKey, 'freeTextValue' => 'moderate'],
|
||||
['questionID' => 'fatigue', 'freeTextValue' => 'severe'],
|
||||
]));
|
||||
|
||||
$res = $this->submitQuestionnaireAs($login['token'], $f->questionnaireId, $f->clientCode, [
|
||||
['questionID' => $f->questionShortId, 'answerOptionKey' => 'yes'],
|
||||
['questionID' => $f->glassSymptomKey, 'freeTextValue' => 'mild'],
|
||||
]);
|
||||
$this->assertApiOk($res);
|
||||
|
||||
[$pdo, $tmpDb, $lockFp] = qdb_open(false);
|
||||
$glass = $pdo->prepare(
|
||||
'SELECT freeTextValue FROM client_answer WHERE clientCode = :cc AND questionID = :qid'
|
||||
);
|
||||
$glass->execute([':cc' => $f->clientCode, ':qid' => $f->glassQuestionId]);
|
||||
$decoded = json_decode((string)$glass->fetchColumn(), true);
|
||||
$this->assertSame(['pain' => 'mild'], $decoded);
|
||||
qdb_discard($tmpDb, $lockFp);
|
||||
}
|
||||
|
||||
public function testResubmitCreatesSecondSubmissionVersion(): void
|
||||
{
|
||||
$login = $this->api()->loginMobileAndGetToken(
|
||||
|
||||
@ -30,6 +30,15 @@ final class AppAnswersTest extends TestCase
|
||||
$this->assertSame(['one', 'two'], qdb_decode_multi_check_values('one, two'));
|
||||
}
|
||||
|
||||
public function testBuildGlassSymptomJsonOmitsEmptyAndReturnsNullForNoSymptoms(): void
|
||||
{
|
||||
require_once dirname(__DIR__, 2) . '/lib/app_answers.php';
|
||||
$this->assertNull(qdb_build_glass_symptom_json([]));
|
||||
$this->assertNull(qdb_build_glass_symptom_json(['pain' => '']));
|
||||
$json = qdb_build_glass_symptom_json(['pain' => 'mild', 'fatigue' => 'severe']);
|
||||
$this->assertSame(['pain' => 'mild', 'fatigue' => 'severe'], json_decode((string)$json, true));
|
||||
}
|
||||
|
||||
public function testGroupAppSubmitAnswersMergesMultiCheck(): void
|
||||
{
|
||||
$answers = [
|
||||
|
||||
Reference in New Issue
Block a user