---
url: https://docs.nemo12.com/architecture/sdd-025-anchor.md
description: >-
  Bản nháp Anchor (SDD-025): quản lý Case và cam kết, dữ liệu, vòng đời Case,
  'không bao giờ để mất cam kết' với gia đình.
---

# SDD-025 — Anchor

*Never lose the commitment.* Thiết kế kỹ thuật cho [PRD-003](../product/prd-003-dory-anchor.md) phần Anchor.

Nếu [Dory](sdd-024-dory.md) là trí nhớ, Anchor là **lương tâm**: nó giữ những thứ Nemo12 đã hứa mà chưa làm, và không cho chúng biến mất một cách êm ái.

## 1. Nguyên tắc

1. **Một lời hứa không có chủ và hạn thì không lưu được.** `owner_user_id` và `due_at` là `NOT NULL` trên `commitments`. Cho phép để trống nghĩa là phần lớn sẽ trống, và một danh sách lời hứa không hạn chính là danh sách những thứ sẽ không xảy ra.
2. **Đóng case cần bằng chứng, không cần một cú bấm.** `resolution_note` bắt buộc; muốn đóng mà chưa có thì phải nói rõ đang đóng bằng ngoại lệ (§6).
3. **Xong chưa chắc là xong.** Mỗi resolution tự đặt một lần **follow-up** (§7). Một vấn đề đóng hôm nay mà hai tuần sau quay lại thì lần đóng đó đã sai.
4. **Escalation cứu việc, không bắt lỗi người.** Không có bảng xếp hạng Dolphin nào sinh ra từ dữ liệu này (PRD-003 §8).
5. **Danh sách ngắn mới có tác dụng.** Anchor cố ý **không** nhận mọi ý nghĩ thoáng qua — thứ đó thuộc về ghi chú của Dory. Vào Anchor là thành nghĩa vụ.

## 2. Dữ liệu

```text
cases
  id · family_id → families · learner_id? → learners
  title · description?
  category(academic|logistics|billing|relationship|technical|other)
  priority(low|normal|high|urgent)
  status(open|in_progress|waiting_family|resolved|closed|reopened)
  opened_by → users · owner_user_id → users
  source_interaction_id? → family_interactions      -- sinh ra từ một cuộc gọi
  due_at?                                            -- SLA tính ra, sửa tay được
  resolved_at? · resolution_note? · resolution_evidence_json?
  closed_at? · reopened_count
  created_at · updated_at

commitments                 -- "chúng tôi sẽ..."
  id · case_id? → cases · family_id → families
  statement                 -- nguyên văn điều đã hứa
  owner_user_id NOT NULL → users
  due_at NOT NULL
  made_at · made_to_contact_id? → family_contacts
  source_interaction_id? → family_interactions
  status(open|kept|missed|cancelled)
  kept_at? · evidence_note?
  created_at · updated_at

case_actions                -- việc cụ thể để xong một case
  id · case_id → cases · title · assignee_user_id? → users
  due_at? · status(todo|doing|done|dropped) · done_at? · created_at

case_followups              -- kiểm lại sau khi đóng
  id · case_id → cases · due_at · question
  status(pending|confirmed_ok|reopened) · checked_by? · checked_at? · note?

case_events                 -- vết xử lý, chỉ nối thêm
  id · case_id · actor_user_id · event_type · from_status? · to_status?
  detail? · created_at
```

**`commitments` tách khỏi `cases` chứ không phải một cột trong đó.** Lý do: rất nhiều lời hứa **không có case nào cả** — "em gửi anh lịch thi thử trước thứ Sáu" không phải một vấn đề, nó là một việc. Bắt phải mở case trước mới ghi được lời hứa là dựng một thủ tục khiến người ta thôi ghi.

**`statement` lưu nguyên văn** điều đã hứa, không phải bản tóm tắt. Khi cãi nhau về việc đã hứa gì, bản tóm tắt là thứ vô dụng nhất.

## 3. Vòng đời Case (REQ-ANC-01)

```text
open → in_progress → waiting_family ⇄ in_progress → resolved → closed
                                                        ↓
                                                    reopened → in_progress
```

`waiting_family` là trạng thái riêng chứ không gộp vào `in_progress`, vì **đồng hồ SLA dừng ở đó** (§5). Không tách thì mọi case chờ phụ huynh trả lời đều trở thành case trễ, và bảng cảnh báo đầy thứ không ai làm gì được — sau vài tuần thì không ai nhìn bảng đó nữa.

`reopened_count` để trần: một case mở lại ba lần là dấu hiệu resolution lần đầu chưa bao giờ đúng, và đó là thông tin đáng nhìn hơn con số "đã đóng bao nhiêu case".

## 4. Commitment (REQ-ANC-02)

Ba đường sinh ra một commitment, xếp theo mức hay dùng:

1. Từ **Interaction Capture** của Dory — vừa gọi xong, bấm "tôi vừa hứa gì đó".
2. Từ trong một **Case**.
3. Tạo thẳng ở màn gia đình.

`status` chỉ có bốn giá trị, và **`missed` là một trạng thái thật, không phải lỗi**. Một hệ thống không cho phép ghi nhận "đã lỡ hẹn" sẽ được dùng bằng cách im lặng đổi `due_at` — và khi đó mọi số liệu về độ tin cậy đều là số giả.

## 5. SLA (REQ-ANC-04)

Hạn tính theo `category` × `priority`, ghi trong config chứ không rải trong mã:

| category | urgent | high | normal | low |
| --- | --- | --- | --- | --- |
| billing | 4h | 1 ngày | 2 ngày | 5 ngày |
| academic | 1 ngày | 2 ngày | 5 ngày | 10 ngày |
| relationship | 1 ngày | 2 ngày | 3 ngày | 7 ngày |
| logistics · technical · other | 1 ngày | 3 ngày | 5 ngày | 10 ngày |

* Đồng hồ **dừng** khi `waiting_family`, chạy lại khi về `in_progress`.
* Cảnh báo **trước** khi trễ, ở mốc 80% thời gian — cảnh báo sau khi trễ chỉ là đưa tin buồn.
* `due_at` sửa tay được, nhưng mỗi lần sửa ghi một dòng `case_events`. Gia hạn im lặng là cách một hệ thống theo dõi hạn tự vô hiệu hoá chính nó.

## 6. Resolution Evidence (REQ-ANC-07)

Chuyển sang `resolved` đòi:

* `resolution_note` không rỗng, **và**
* ít nhất một trong: một `case_action` đã `done`, một `commitment` đã `kept`, hoặc một `family_interaction` gắn với case này.

Thiếu thì API trả `409` kèm câu nói rõ còn thiếu gì — cùng khuôn `publish_blockers` đã dùng ở [SDD-019](sdd-019-mentor-albums.md) §4 và [SDD-020](sdd-020-school-registry.md) §4. Khuôn này đã chứng minh được: người dùng biết trước phải làm gì thay vì bấm rồi mới nhận lỗi.

**Có một đường đóng ngoại lệ**: `closed` với `resolution_note` bắt đầu bằng `[no-evidence]`. Nó tồn tại vì thực tế có case chết già (gia đình ngừng học, vấn đề tự hết) và một cổng không có lối thoát nào là một cổng người ta sẽ tìm cách lách. Nhưng những case đó **hiện thành một mục riêng** trong Commitment Model (`closed_without_evidence`) — hệ thống tự thú nhận chỗ nó đang bị dùng sai.

## 7. Follow-up (REQ-ANC-05)

Mỗi lần `resolved`, hệ tự tạo một `case_followup` với `due_at` = +14 ngày (billing +7). `question` sinh từ tiêu đề case: *"Two weeks on, is \[title] genuinely settled?"*

Đến hạn, follow-up hiện trong danh sách việc của owner. Ba kết cục: `confirmed_ok` · `reopened` · gia hạn một lần. **Không có nút bỏ qua** — bỏ qua chính là thứ follow-up sinh ra để chặn.

## 8. Escalation (REQ-ANC-06)

| Điều kiện | Hành động |
| --- | --- |
| Case quá `due_at` 24h | gắn cờ `at_risk`, hiện đầu danh sách của owner |
| Case quá `due_at` 72h | thêm vào bảng của trưởng nhóm |
| Commitment quá hạn | đánh dấu `missed`, sinh signal cho Dory (nhà này ta đã lỡ hẹn) |
| Case `urgent` chưa ai nhận sau 4h | vào bảng trưởng nhóm ngay |

Chạy trong cron chung với [Family Signals](sdd-024-dory.md) §5. Mỗi lần escalate ghi `case_events` — **có vết thì mới cãi lại được**, kể cả cãi rằng luật escalation đang quá nhạy.

## 9. Audit (REQ-ANC-08)

`case_events` là vết **nghiệp vụ** (đọc được trên màn hình, kể lại câu chuyện của một case). `audit_log` là vết **truy cập** (ai xem dữ liệu gia đình nào lúc nào, AS-07.4). Hai thứ khác nhau và cùng phải có; gộp lại thì một trong hai sẽ bị cắt xén cho vừa cái kia.

`case_events` chỉ nối thêm, không có đường sửa hay xoá trong ứng dụng — cùng luật đã áp cho `audit_log`.

## 10. API

| Method | Path | Quyền |
| --- | --- | --- |
| GET · POST | `/v1/mentor/cases` | 🔐 role `mentor` |
| GET · PATCH | `/v1/mentor/cases/{id}` | 🔐 role `mentor` |
| POST | `/v1/mentor/cases/{id}/resolve` | 🔐 role `mentor` |
| POST | `/v1/mentor/cases/{id}/reopen` | 🔐 role `mentor` |
| POST | `/v1/mentor/cases/{id}/actions` · PATCH `/v1/mentor/actions/{id}` | 🔐 role `mentor` |
| GET · POST | `/v1/mentor/commitments` | 🔐 role `mentor` |
| PATCH | `/v1/mentor/commitments/{id}` | 🔐 role `mentor` |
| GET | `/v1/mentor/my-work` | 🔐 role `mentor` |
| POST | `/v1/mentor/followups/{id}/check` | 🔐 role `mentor` |
| GET | `/v1/mentor/escalations` | 🔐 role `mentor` |

`/v1/mentor/my-work` là màn hình mở đầu ca trực: commitments đến hạn · cases at-risk · follow-ups tới hạn, gộp một truy vấn.

## 11. Cái chưa làm

| Việc | Vì sao |
| --- | --- |
| Tự nhắn cho phụ huynh khi commitment tới hạn | Anchor nhắc Dolphin, không thay Dolphin nói chuyện (PRD-003 §8) |
| Bảng xếp hạng Dolphin theo tỉ lệ giữ lời | Escalation để cứu việc, không để bắt lỗi người |
| SLA theo lịch làm việc (trừ cuối tuần) | Đợt đầu tính theo giờ trôi; thêm lịch làm việc là một lớp phức tạp đáng làm sau khi biết dữ liệu thật trông thế nào |
| Case template theo loại vấn đề | Chờ đủ dữ liệu thật để biết loại nào lặp lại |

## Trace

REQ-ANC-01→§2-3 · REQ-ANC-02→§4 · REQ-ANC-03→§2 · REQ-ANC-04→§5 · REQ-ANC-05→§7 · REQ-ANC-06→§8 · REQ-ANC-07→§6 · REQ-ANC-08→§9.
