feat(questionnaire): integrate revised intake and follow-up monitoring
CI / ruff · pytest (pull_request) Successful in 55s

Add gated TSQ, Russian C-SSRS Recent, durable responses and notifications, diaries, schedules and audited clinical review. Preserve manual assessment when the Russian follow-up source is unavailable.

Refs #52
This commit is contained in:
Pavel Eliseev
2026-10-03 18:19:34 +03:00
parent 8b61eecf1b
commit c42ef621e5
34 changed files with 2556 additions and 85 deletions
+24
View File
@@ -42,6 +42,20 @@ def menu_reply(ctx: Ctx, text: str | None = None) -> Reply:
[Button(label=b.contact_doctor, data="menu:contact_doctor")],
[Button(label=b.revoke, data="consent:revoke")],
]
from app.core.intake_questions import texts
from app.core.intake_runtime import intake_enabled
if intake_enabled():
buttons.insert(0, [Button(label=texts()["help"], data="intake:help")])
buttons.insert(
0,
[
Button(
label=texts()["resume"] if ctx.data.get("questionnaire_record") else texts()["start"],
data="intake:resume" if ctx.data.get("questionnaire_record") else "intake:start",
)
],
)
return Reply(text=text or ctx.texts.idle.menu_prompt, buttons=buttons)
@@ -53,6 +67,10 @@ async def onboarding_entry(ctx: Ctx, after: str | None = None) -> str:
profile (after `after`, if given) that has such a state registered; `"idle"` when none is left."""
profile = await ctx.profile()
modules = list(profile.modules) if profile is not None and profile.modules else []
from app.core.intake_runtime import intake_enabled
if after is None and intake_enabled() and any(m in modules for m in ("veteran", "depression")):
return "intake.start"
if after in modules:
modules = modules[modules.index(after) + 1 :]
for module in modules:
@@ -275,6 +293,8 @@ async def _reach_out(ctx: Ctx, event: Event) -> Step:
def close_crisis(ctx: Ctx) -> Step:
"""Doctor acknowledged the crisis: clear the flag; a C-SSRS still running returns to idle afterwards."""
if not (ctx.data.get("crisis") or {}).get("open") and ctx.state != "crisis":
return Step()
crisis = {**(ctx.data.get("crisis") or {}), "open": False, "closed_at": ctx.now.isoformat()}
scales = {
sid: {**s, "return_state": "idle"} if s.get("return_state") == "crisis" else s
@@ -311,7 +331,11 @@ async def revoke(ctx: Ctx) -> Step:
"""
async with get_identity_session() as s:
await identity_repo.revoke_consent(s, token=ctx.token, now=ctx.now)
from app.services.questionnaire import cancel_pending
await cancel_pending(ctx.session, ctx.token)
data = {
"questionnaire_record": None,
"consent": False,
"revoked_at": ctx.now.isoformat(),
"delete_requested": True,