mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 22:50:51 +00:00
fix(onboarding): derive the module map only from a questionnaire that was finished
The only gate on the derivation was Q1 being set, so one answer was enough to turn the whole questionnaire into a module map. Two ways that landed wrong and they compound. Somebody answers who the record is for, closes the tab, and later finishes the legacy four-step wizard, which posts to this same endpoint and knows nothing about these questions: the five unanswered ones each read as their conservative default, eleven surfaces are written off, and the once-only marker makes it unrepeatable. Or any client patches one answer and posts the confirm straight back. The gate now asks whether every question has an answer or a deliberate pass. A half-answered flow derives nothing, is not stamped complete, and keeps its confirm step pending, so finishing the questions later still does the work. The legacy arm is untouched: a caller with no setup row still answers the fixed acknowledgement it always did.
This commit is contained in:
1 parent
7a1f3fe32d
commit
cc6e6ca221
6 files changed
+165
-12
No files matched your search
@@ -127,13 +127,18 @@ function statusOf(state: OnboardingStateDto, id: OnboardingStepId) {
|
||||
return state.steps.find((step) => step.id === id)?.status;
|
||||
}
|
||||
|
||||
/** The five answers a "blood pressure and medication" setup gives. */
|
||||
/**
|
||||
* The answers a "blood pressure and medication" setup gives — all six
|
||||
* questions, because the confirm screen is only reached once every one of them
|
||||
* has an answer or a deliberate pass.
|
||||
*/
|
||||
async function answerTheQuestions() {
|
||||
await patchAnswer({ step: "who", recordTarget: "me" });
|
||||
await patchAnswer({ step: "areas", areas: ["blood-pressure", "labs"] });
|
||||
await patchAnswer({ step: "medication", medication: "yes" });
|
||||
await patchAnswer({ step: "sources", status: "skipped" });
|
||||
await patchAnswer({ step: "visit", visit: "no" });
|
||||
await patchAnswer({ step: "units", status: "skipped" });
|
||||
}
|
||||
|
||||
async function modulePrefs(userId: string) {
|
||||
@@ -339,6 +344,40 @@ describe("POST /api/onboarding/complete", () => {
|
||||
expect(profile.cycleTrackingEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it("derives nothing from a questionnaire that was never finished", async () => {
|
||||
const user = await makeUser("half");
|
||||
await signIn(user.id);
|
||||
// Q1 and nothing else — the tab was closed, or the legacy four-step wizard
|
||||
// posted here afterwards. Either way the remaining five questions have no
|
||||
// answer, and reading them as their conservative defaults would switch off
|
||||
// eleven surfaces and latch the result.
|
||||
await patchAnswer({ step: "who", recordTarget: "me" });
|
||||
|
||||
const res = await postComplete();
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const row = await getPrismaClient().user.findUniqueOrThrow({
|
||||
where: { id: user.id },
|
||||
select: { modulePreferencesJson: true },
|
||||
});
|
||||
expect(row.modulePreferencesJson).toBeNull();
|
||||
|
||||
const record = await getPrismaClient().onboardingRecord.findUniqueOrThrow({
|
||||
where: { userId: user.id },
|
||||
});
|
||||
expect(record.modulesDerivedAt).toBeNull();
|
||||
expect(record.completedAt).toBeNull();
|
||||
|
||||
const state = await readState(res);
|
||||
expect(statusOf(state, "confirm")).toBe("pending");
|
||||
|
||||
// Finishing the questions later still derives, so the gate defers the
|
||||
// work rather than losing it.
|
||||
await answerTheQuestions();
|
||||
await postComplete();
|
||||
expect((await modulePrefs(user.id)).medications).toBe(true);
|
||||
});
|
||||
|
||||
it("leaves a record that never answered the questions exactly as it was", async () => {
|
||||
const user = await makeUser("legacy");
|
||||
await signIn(user.id);
|
||||
|
||||
Reference in new issue
Block a user