Skip to content

Business Logic & Dataflow ​

Compact reference for Digital Thai MoCA / BrainDI system behavior.

Actors & Components ​

ActorComponentPurpose
Clinical staffBackoffice (braindi-backoffice-frontend)Create tests, review results, manual grading, PDF export
PatientPatient webapp (automoca-web-app)Take MoCA test on tablet/browser
System workersautomoca-aiservice + global-asr-serviceAuto-score sections & transcribe audio (no UI)

Shared backend: automoca-api-services (braindi-backend).

Test Lifecycle ​

Key Statuses ​

StatusMeaning
createdTest link exists; patient has not started testing.
testingPatient is currently in or has started the test flow.
predictingPatient called POST /web/tests/end; test is closed. Backend waits for in-flight AI/ASR results (or cron timeout) before waiting for review. (Some test result still missing, or haven't come back from AI)
waiting for reviewTest is ready for staff review/scoring.
finishedAll required scores are complete with no review required, or staff review is finalized; the result is downloadable.

Backoffice progress label while a patient is testing ​

Backoffice progress label

The backoffice progress label is based on the test's latest successfully submitted lastStep while status = testing. It is not a direct report of the patient webapp screen currently open. Consequently, the label intentionally lags behind the patient by one submitted item or domain boundary.

While status = created, the dashboard label is always รอทดสอบ regardless of lastStep.

Progress is persisted in two locations by automoca-api-services:

  • MongoDB: root field lastStep on the test document. Backoffice test records and progress labels use this persisted test data.
  • Redis: JSON field last_step under key test:<testId>. Patient token validation and resume routing use this cache record. Progress updates retain the key's existing TTL.

The backend (automoca-api-services) persists lastStep on each successful submit. The backoffice does not compute progress itself; it maps the API's last_step field to a domain label via STEP_TO_CATEGORY in braindi-backoffice-frontend (src/dashboard/constant.ts).

While status = testing, each persisted lastStep maps to a backoffice label as follows:

lastStep (backend)Backoffice labelWhen backend sets it
create_testVisuospatialStaff creates test (CreateTest). Redis cache also starts here on validate-token for a new session.
patient_detailVisuospatialPatient POST /web/tests (patient detail / consent)
visuospatial_1VisuospatialPUT /web/tests/visuospatial with test = visuospatial_1 (first submit that sets status = testing)
visuospatial_2VisuospatialPUT /web/tests/visuospatial with test = visuospatial_2
visuospatial_3VisuospatialPUT /web/tests/visuospatial with test = visuospatial_3
naming_1NamingPUT /web/tests/naming with test = naming_1
naming_2NamingPUT /web/tests/naming with test = naming_2
naming_3NamingPUT /web/tests/naming with test = naming_3
memory_1MemoryPUT /web/tests/memory with test = memory_1
memory_2MemoryPUT /web/tests/memory with test = memory_2
attention_1AttentionPUT /web/tests/attention/number with test = attention_1
attention_2AttentionPUT /web/tests/attention/number with test = attention_2
attention_3AttentionPUT /web/tests/attention/tap with test = attention_3
attention_4AttentionPUT /web/tests/attention/subtract with test = attention_4
language_1LanguagePUT /web/tests/language/repeat with test = language_1
language_2LanguagePUT /web/tests/language/repeat with test = language_2
language_3LanguagePUT /web/tests/language/fluency with test = language_3
abstraction_1AbstractionPUT /web/tests/abstraction with test = abstraction_1, only after both answers are submitted
abstraction_2AbstractionPUT /web/tests/abstraction with test = abstraction_2, only after both answers are submitted
delayed_recallDelayed RecallPUT /web/tests/orientation for answers 1–5 (see notes below — not set by delayed-recall endpoints)
orientationOrientationPUT /web/tests/orientation answer 6, when all orientation answers are complete
endOrientationPOST /web/tests/end (status becomes predicting immediately; label switches to ประมวลผล)

After POST /web/tests/end: predicting → ประมวลผล, then waiting for review → รอประเมิน or finished → เสร็จสิ้น.

Implementation-specific progress behavior (verified in submit.test.service.go and acceptance_tests/happy_path.go):

  • Abstraction partial submits: a single answer on abstraction_1 or abstraction_2 does not update lastStep. Until both answers for that item are in, the label stays on the previous step (language_3 or abstraction_1).
  • Delayed recall does not advance lastStep: PUT /web/tests/delayed_recall and PUT /web/tests/delayed_recall/hint never change lastStep. While the patient is in delayed recall (including hints/MCQ), MongoDB lastStep remains abstraction_2 and the backoffice shows Abstraction.
  • delayed_recall is an orientation-progress marker: the backend sets lastStep = delayed_recall on each orientation submit for answers 1–5. The backoffice therefore shows Delayed Recall while the patient is answering orientation questions 1–5, not during the actual delayed-recall section.
  • Orientation completion: after orientation answer 6, lastStep becomes orientation. The patient webapp then immediately calls POST /web/tests/end, so Orientation is rarely visible as an active testing label.
  • Patient resume (Redis): while orientation answers 1–5 are in flight, Redis last_step stays delayed_recall; after answer 6 it becomes orientation until /end sets end.

Auth Model ​

TokenUsed byProtects
Staff bearerBackoffice/tests/*, /patients/*, /users/*, /usergroups, /web/tests/generate, /web/tests/staff-update-patient-detail, /web/auth/logout
Test bearerPatient webappGET /web/auth/validate-token, /web/tests/* (submit answers, upload files, live speech)

401 on backoffice → redirect to /login (axios response interceptor).

Staff login (POST /web/auth/login) returns a JWT stored in backoffice local storage. Backend also stores role and accessible organization IDs in a Redis session keyed by user_id. Staff JWTs contain user_id and signed_time (no JWT exp); validation also requires the Redis entry (session_expiration_hour in backend config). Logout (POST /web/auth/logout) deletes that Redis entry.

Patient access uses a separate test JWT embedded in the generated link (?token=). The webapp validates it via GET /web/auth/validate-token, then reuses it as the bearer for /web/tests/*. Test JWTs contain encrypted_id, test_id, and signed_time.

Patient Webapp Test Page Flow (automoca-web-app) ​

Logical 22-step MoCA sequence (excludes backoffice). The patient webapp has additional intro, tutorial, and retry screens beyond these 22 scored steps.

Scoring runs during the test (per-section Kafka AI and async ASR), not at the thank-you page. POST /web/tests/end only closes the test and sets status = predicting.

Scored total (MoCA): 30 pts — Memory (trials 1–2) is learning only and does not count toward total score. Only delayed recall round 1 (free recall) counts toward MoCA total (0–5 pts).


MoCA Test Domains — Detail ​

1. Visuospatial / Executive — 5 pts ​

PageTaskPatient actionAI scoringPoints
3Trail makingDraw path connecting circles in ordervisuospatial_1 — OpenCV path analysis on drawing + stroke coordinates0 or 1
4Cube drawingCopy 3D cube from referencevisuospatial_2 — Gemini multi-score + logistic regression pass/fail0 or 1
5Clock drawing (11:10)Draw clock face with handsvisuospatial_3 — Gemini evaluates 3 components1 + 1 + 1

Clock sub-scores (visuospatial_3):

  • score_1 — contour closed + circular
  • score_2 — numbers 1–12 complete
  • score_3 — hands point to 11 and 2

Photo upload fallback → AI marks unsure, staff must grade manually.


Audio check (page 6) — not scored ​

PageTaskPatient actionScoring
6Audio checkConfirm can hear doctor's voice (speak any words)No score — mic/ASR gate only

Occurs after visuospatial, before naming. Patient must speak so real-time ASR detects words. If nothing is detected, the patient webapp loops between mic-test and retry pages — no skip path. User cannot proceed until ASR succeeds.


2. Naming — 3 pts ​

PageTaskPatient actionAI scoringPoints
7สิงโต (lion)See image → speak namenaming_1 — live WSS transcript match0 or 1
8แรด (rhino)See image → speak namenaming_20 or 1
9อูฐ (camel)See image → speak namenaming_30 or 1

Live WSS speech during test; on submit the webapp sends audio plus transcribe_text to PUT /web/tests/naming, and the backend publishes AI scoring immediately (naming_1–naming_3). Naming does not use the async ASR Kafka pipeline.


3. Memory — not scored ​

Two separate patient screens (memory_1, then memory_2), both via PUT /web/tests/memory with different test values.

TrialPatient actionAPIScoring
memory_1App plays 5 words, patient recalls all 5PUT /web/tests/memoryNo score
memory_2Second recall of the same 5 wordsPUT /web/tests/memoryNo score

Words: หน้า, ผ้าไหม, วัด, มะลิ, สีแดง — same set reappears in Delayed Recall.

Learning phase only. Backoffice may display memory audio/transcripts for review but neither trial adds to MoCA total.


4. Attention — 6 pts ​

PageTaskPatient actionAI / rule scoringPoints
1121854Repeat digits forwardattention_1 — transcript match0 or 1
12742Repeat digits backwardattention_2 — transcript match0 or 1
13Tap-1 (เคาะนิ้ว)Tap when hearing "1" in digit streamRule-based tap detection (29 digits)0 or 1
14100 − 7Answer 100 − 7, then keep subtracting 7 until timeoutattention_4 — ASR + Gemini0–3 cumulative

Patient flow: User answers what 100 − 7 is (93), then continues subtracting 7 on each step until the recording times out. The patient webapp does not stop after a fixed number of answers.

Grading: Backoffice scoring uses only the first 5 answers (93 → 86 → 79 → 72 → 65). Additional answers recorded before timeout are stored but not counted toward the MoCA score.

Pages 11 and 12 use real-time WSS speech during the test. Page 14 (100 − 7) uses local recording only (no WSS); attention_4 AI scoring runs on the uploaded audio after submit.


5. Language — 3 pts ​

PageTaskPatient actionAI scoringPoints
15จอมRepeat sentence: ฉันรู้ว่าจอมเป็นคนเดียวที่มาช่วยงานวันนี้language_10 or 1
16แมวRepeat sentence: แมวมักซ่อนตัวอยู่หลังเก้าอี้เมื่อมีหมาอยู่ในห้องlanguage_20 or 1
17ก.ไก่Name words starting with ก within time limitlanguage_3 — word count + Thai prefix rules0 or 1

6. Abstraction — 2 pts ​

PageTaskPatient actionAI scoringPoints
18รถไฟ + จักรยานState similarity (2 audio answers)abstraction_1 — allowed-answer match0 or 1
19ไม้บรรทัด + นาฬิกาState similarity (2 audio answers)abstraction_2 — allowed-answer match0 or 1

7. Delayed Recall — 5 pts ​

Three phases across multiple patient screens (free recall, then per-word category hints, then per-word multiple choice). Words: หน้า, ผ้าไหม, วัด, มะลิ, สีแดง (same as memory trials).

PhaseSupportPatient flowAI taskCounts toward MoCA?
1 — Free recallตอบได้เองSay all 5 words at once. All correct → skip to orientation; else → phase 2delayed_recall_1Yes
2 — Category hintใบ้หมวดหมู่Per missed word: play hint audio → wait for answer. After last missing word: all correct → skip; else → phase 3delayed_recall_2No
3 — Multiple choiceตัวเลือกPer still-wrong word: MCQ via audio → answer. Always proceeds to orientation even if words remain wrongdelayed_recall_3No

MoCA scoring (round 1 only):

  • AI scores each word 0 or 1 (score_1–score_5) by matching allowed answers in the transcript.
  • Round 1: single transcript checked for all 5 words. Rounds 2/3: per-word transcripts.
  • MoCA total uses delayed_recall_1 only (sum of 5 word scores, 0–5 pts). Backend score.Total and backoffice grader both use round 1.
  • Rounds 2 and 3 are stored for staff review but do not change MoCA total.

Siriraj auxiliary metric (MisTotal): 3×DR1 + 2×DR2 + DR3 — exported separately, not used in MoCA total. DR2/DR3 scores cascade: only words still wrong from the prior round are counted.


8. Orientation — 6 pts ​

#QuestionPatient actionAI taskPoints
1วันนี้วันที่เท่าไรSpeak answerorientation_10 or 1
2เดือนนี้เดือนอะไรSpeak answerorientation_20 or 1
3ปีนี้ปีพ.ศ.อะไรSpeak answerorientation_30 or 1
4วันนี้วันอะไรSpeak answerorientation_40 or 1
5ที่นี่ที่ไหนSpeak answerorientation_50 or 1
6จังหวัดอะไรSpeak answerorientation_60 or 1

Ground truth from test end timestamp + patient geolocation.


MoCA score summary ​

DomainMax ptsCounts toward total?
Visuospatial5Yes
Naming3Yes
Memory—No (learning only)
Attention6Yes
Language3Yes
Abstraction2Yes
Delayed Recall5Yes (round 1 free recall only)
Orientation6Yes
Total30

MoCA System Flow ​

DTM system flow


Patient Webapp Test Flow (automoca-web-app) ​

AI Scoring Tasks ​

automoca-aiservice routes Kafka messages by type:

GroupTask types
Visuospatialvisuospatial_1, visuospatial_2, visuospatial_3
Namingnaming_1, naming_2, naming_3
Attentionattention_1, attention_2, attention_4 (attention_3 tap-1 is rule-based in backend only)
Languagelanguage_1, language_2, language_3
Abstractionabstraction_1, abstraction_2
Delayed recalldelayed_recall_1, delayed_recall_2, delayed_recall_3
Orientationorientation_1 … orientation_6

Each task reads media from S3, runs scorer (rules / Gemini / ResNet), returns score payload via Kafka.

Staff Backoffice Flow (braindi-backoffice-frontend) ​

Backoffice grading modules: attention, naming, memory, visuospatial, language, abstraction, delayed-recall, orientation.

Staff can override AI scores per section. When the last unreviewed section is confirmed, backoffice calls POST /tests/score with all section data to persist the final total and set status = finished.

Backoffice Roles & Access Control ​

The system uses three technical role values. The backoffice displays different labels for two of them:

Backoffice labelTechnical rolePrimary scope
CreatoruserTests created by that account in its primary organization
AdminadminTests and users in the Admin's assigned organizations
OrganizersuperuserAll organizations

Each staff account has one primary usergroupId. An Admin may also have accessibleUsergroupIds for additional organizations. The primary organization is always included in the effective access list. Admin accounts without additional IDs, including legacy accounts, fall back to their primary organization.

Organization selection ​

  • Creator: no organization selector; the dashboard and generated tests use the account's primary organization.
  • Admin with one organization: no organization selector; the primary organization is used.
  • Admin with multiple organizations: the organization selector lists only assigned organizations.
  • Organizer: the selector lists all organizations and is always shown; defaults to the account's primary organization when the URL has no org filter.
  • The backoffice dashboard displays one organization at a time. Admin requests without an organization default to the primary organization. The backend permits Organizer test search without an organization filter.
  • Only Organizer can replace an Admin's additional organization access through the API. That operation preserves the primary organization and refreshes an active Redis session immediately.

Test and backoffice capabilities ​

Unless a row says otherwise, “own” means a test whose stored creator user ID matches the logged-in account, and “assigned” means the test's organization is in the Admin's effective access list.

CapabilityCreator (user)Admin (admin)Organizer (superuser)
Log in to backofficeYesYesYes
List organizationsPrimary onlyAssigned organizationsAll organizations
Generate a test linkPrimary organizationSelected assigned organizationSelected organization
Search dashboard testsOwn onlySelected assigned organizationSelected organization in UI; any/all through API
Open a resultOwn onlyAssigned organizationsAny
Download test mediaOwn onlyAssigned organizationsAny
Review and override scoresOwn onlyAssigned organizationsAny
Edit patient demographicsOwn onlyAssigned organizationsAny
Edit result-page note in UINoYes, except when the Admin's primary organization is SirirajYes
Update note through APIOwn onlyAssigned organizationsAny
Generate/download PDFOwn onlyAssigned organizationsAny
Delete testNoAssigned organizations except SirirajAny, including Siriraj

Creator ownership remains the access boundary throughout the test lifecycle. A Creator cannot open or modify another Creator's test even when both accounts have the same primary organization. Admin access is organization-based and is not limited to tests created by that Admin.

The dashboard result action ("ตรวจ") is available only when the test is waiting for review or finished. The /result/:id route itself has no status guard — direct URL access still loads the page.

Siriraj behavior ​

Siriraj is the only organization with explicit role/action exceptions in the current implementation:

  • The backoffice hides manual test-link generation whenever Siriraj is selected, for every role. The test-creation API has no equivalent Siriraj-specific check and otherwise applies the normal role scope.
  • Creator cannot delete any test. Admin cannot delete a Siriraj test, enforced by the backend even when Siriraj is assigned to that Admin. Organizer can delete a Siriraj test.
  • Result-page note editing checks the Admin's primary organization, not the result's organization. A primary-Siriraj Admin cannot edit notes in the UI. A multi-organization Admin whose primary organization is not Siriraj can edit a Siriraj note in the UI. The backend note-update endpoint applies normal assigned-organization access and has no Siriraj-specific note rule.
  • Other patient fields remain editable for a role that can access the result.
  • Scoring a Siriraj test automatically generates and submits its result to the Siriraj integration. Changing education on an already finished Siriraj test also triggers resubmission.

User and organization management APIs ​

There is no user-management screen in braindi-backoffice-frontend. The following routes are active in automoca-api-services and require a staff bearer token:

Method and runtime pathCurrent authorization
POST /usersCreator denied. Admin may create only Creator accounts in assigned organizations. Organizer may create Creator or Admin accounts, but not Organizer accounts. Additional accessible organizations are accepted only when the new account is an Admin.
GET /users/:idCreator may read self. Admin may read users in assigned organizations. Organizer may read any user.
PUT /usersCreator may target self. Admin may target users whose primary organization matches the Admin's primary organization. Organizer may target any user. Accepted mutable fields are username, password, first name, last name, and phone number.
GET /users/usergroup/:usergroup_idCreator receives only self for the primary organization. Admin may list users in an assigned organization. Organizer may list users in any organization.
PUT /users/:id/accessible-usergroupsOrganizer only; target must be an Admin. Replaces additional organization access while retaining the Admin's primary organization.
POST /usergroupsOrganizer only; creates an organization/usergroup.
GET /usergroupsCreator receives the primary organization, Admin receives assigned organizations, and Organizer receives all organizations.

Current operations update staff information for every role directly in MongoDB rather than through a backoffice screen. Staff offboarding is also a manual operations/database process.

The active management APIs do not provide operations to:

  • create another Organizer account;
  • delete, deactivate, reactivate, or lock a staff account;
  • change an account's role, email, or primary organization;
  • administratively revoke a staff session;
  • rename or delete an organization.

Manual database changes do not automatically refresh or delete an existing Redis session. The cached role and organization access remain in use until the cache expires, the user logs out, or operations remove/update the Redis entry.

Authorization implementation facts ​

  • Backoffice route components do not have role-specific route guards. UI controls are conditionally displayed, while staff API endpoints apply their own backend authorization checks.
  • PUT /patients and GET /patients/tests/:id require a valid staff bearer token but do not apply role, creator, or organization checks in the patient service.
  • The staff route POST /tests/files is not usable with a staff bearer token — UploadFile requires test bearer context (test_id, encrypted_id). Patient uploads use POST /web/tests/files.
  • Tests retain the creating user ID and username, but the system has no structured audit history recording which staff account later changed scores or patient data, generated a PDF, or deleted a test.

Core Data Entities ​

MongoDB collections used by automoca-api-services:

CollectionGo modelPurpose
usergroupsUsergroupOrganization / clinic
usersUserStaff accounts
testsTestOne MoCA session (demographics + all scores)
patientsPatientLinks encryptedId to related test IDs only

Test media (audio, images, drawings, PDFs) are stored in S3 under {testId}/{filename}. The test document stores path references inside embedded section data fields — there is no separate files collection.

Demographics and session context are stored on tests, not patients. The PATIENT collection is only an encryptedId → tests[] index. Education appears twice on a test document: as root field education (enum used by APIs/UI) and inside result.education / predictResult.education (scoring shape), both written together on POST /web/tests patient-detail submit.

encryptedId is duplicated on patients and tests (same string, no FK). DTM backoffice link generation sets encryptedId = "backoffice-generated" for all tests; Siriraj passes a real per-patient id from the external system.

Entity summaries ​

EntityDescription
UsergroupOrganization record. Primary identifier in APIs is organization (display name), not a separate name field.
UserStaff account (user, admin, or superuser). One primary usergroupId; Admins may have additional accessibleUsergroupIds.
PatientLightweight index: encryptedId plus tests[] (ObjectIds). Created/updated when a test link is generated or a patient session starts. Does not store demographics.
TestOne MoCA session. Holds patient demographics (dateOfBirth, education, sex, location, province, note, …), workflow fields (status, lastStep, endTestTime), staff context (userId, usergroupId, username), patient link (encryptedId), and two embedded result trees. Soft-deleted via deletedAt.

Embedded section results (result and predictResult) ​

There is no section_scores table. Each test document embeds a TestResult object twice:

FieldWritten byUsed for
predictResultPatient submits, AI consumers, async ASR callbacksAI predictions and in-flight scoring
resultStaff review (POST /tests/score) and some rule-based submitsStaff-confirmed / final values shown in backoffice and PDF

Both trees share the same shape (TestResult in test/entity/test-result.model.go). Top-level score is the total MoCA points (0–30). Cognitive level (NORMAL / MCI / AD) is derived from that total at read time (IntToScoreLevel) — it is not stored on the test document.

Per-item keys inside result / predictResult (22 MoCA items + education):

visuospatial_1–visuospatial_3, naming_1–naming_3, memory_1–memory_2, attention_1–attention_4, language_1–language_3, abstraction_1–abstraction_2, delayed_recall_1–delayed_recall_3, orientation, education

Each item object has:

  • scores — per-question points and flags (score_1, unsure_1, is_confirmed_1, …; shape varies by item)
  • data — submitted answers (audio filename, image, drawing path, tap sequence, geolocation, transcript text, …)

Staff review updates result.*; predictResult retains what the AI pipeline produced unless overwritten by the same submit path.

Redis caches (not MongoDB) ​

Key patternContents
test:<testId>Patient session: encrypted_id, last_step (resume routing)
User session cacheStaff JWT session: role, accessible usergroup IDs

Score Interpretation ​

Backoffice maps total score to cognitive level:

LevelMeaning
ADAlzheimer's disease range
MCIMild cognitive impairment
NORMALNormal range

Education level (Thai labels in UI) adjusts clinical context for staff review.

Async vs Real-Time Processing ​

WhenWhat runsExamples
During test (WSS)Real-time ASR for patient UXNaming, memory, attention 1–2, language repeat, delayed-recall hints
During test (Kafka AI)Section scoring on submitVisuospatial, naming, attention 1/2/4, delayed recall
During test (Kafka async ASR → AI)Transcribe then scoreLanguage repeat/fluency, abstraction, orientation
During test (local rules)Immediate backend scoringMemory, attention 3 (tap-1)
At /endClose test onlySets predicting; does not start new Kafka jobs

Cron job monitors delayed AI predictions → SNS alarm if threshold exceeded.

Multi-Brand Behavior ​

Same backend serves multiple frontends (DTM, BrainDI, Siriraj) via configured frontend URLs. Business logic is shared; branding and domain differ per deployment.