Giao diện
Feature Inventory — SAPP-26297: Chuyển nhượng học viên (tool test sapp.011) ​
Kiểm kê tính năng đã triển khai, dựng từ code. Đầu vào cho FS as-built (Mode B). Mọi dòng có bằng chứng
file:dòng(đường dẫn tính từ root repomind_keep/); không có bằng chứng → §8.Công thức truy vết:
ba/system-profile.md§3. Cơ chế dùng chung (ToolGate / quyền tool,env_systems, auto-login Ops, Ops passthrough transport, Ops SQL runner, lưu trạng thái tool) chỉ ghi cách 26297 dùng, bên trong ghi "→ nền tảng" (xemfeature-inventory-platform-20260930.md).
| Field | Value |
|---|---|
| Project | ba/projects/2026-SAPP-Works-tools |
| Mode | B – As-built |
| Ngày đọc code | 2026-09-30 |
| Commit | e77c47a |
| Điểm vào BA cung cấp | Sub-tool sapp-26297 (tool code sapp.011, "SAPP 26297: Chuyển nhượng học viên"), màn /works/sapp/tools/sapp-26297, route /api/v1/sapp/{dev|staging}/sapp-26297/* |
| Thành phần chạm tới | FE · BE (api) · hệ ngoài: SAPP Ops API, SAPP Ops DB (qua SQL runner), SAPP LMS API, HubSpot (portal dev/staging) |
| Status | Draft |
1. Phạm vi đã truy vết ​
- Trong phạm vi:
- Màn
/works/sapp/tools/sapp-26297— fixture, chọn môi trường, 9 tab luồng (Chuẩn bị, 1, 2, 3A, 3B, 3C, 4, 5, 6), 78 case, 3 loại bước (gọi API tự chấm / chạy SQL catalogue / hướng dẫn tay). - 14 thao tác BE × 2 môi trường = 28 route
{dev|staging}/sapp-26297/*(6 Ops, 1 LMS, 7 HubSpot). - Catalogue SQL
s26297.*(39 entry: 20 chỉ-đọc, 19 ghi) và cách tool gọi SQL runner. - Seed tool
sapp.011.
- Màn
- Ngoài phạm vi (dừng tại đây → nền tảng):
ToolGate/ trạng thái tool trong shell,SappAuthCommonService.withToken(auto-login Ops),SappOpsService.request(transport),EnvSystemMapCommonService,HubSpotCommonService(chỉ ghi hành vi 26297 phụ thuộc), SQL runnersapp-ops/sql/*(FlowStepSqlcủa 26878,SappOpsSqlCommonService),fillTemplate/OpsBagcủa 26878. Nghiệp vụ phía SAPP (lms-be, hubspot-service, logic chuyển nhượng thật) — tool chỉ gọi và chấm, không cài.
2. Điểm vào (Entry points) ​
Mọi route BE nằm dưới prefix /api/v1/sapp/; mỗi thao tác có 1 route dev/… và 1 route staging/…, không có route production.
| ID | Loại | Tên / đường dẫn người dùng thấy | Bằng chứng |
|---|---|---|---|
| E-01 | Màn web | /works/sapp/tools/sapp-26297 — sidebar nhóm "SAPP 26297 - Chuyển nhượng học viên", mục "Test theo luồng" | fe/app/works/sapp/tools/sapp-26297/page.tsx:9-10 · fe/src/ui/pages/works/sapp/tools/registry.ts:104-110 · fe/src/ui/components/works/sapp/sapp-26297-tool.tsx:178 |
| E-02 | API (Ops, đọc) | GET {env}/sapp-26297/classes/:classId/transfer-candidates → Ops GET /api/v1/classes/:id/transfer-candidates | be/apps/api/src/sapp/sapp-26297.controller.ts:78,103 · be/libs/constants/src/lib/sapp/sapp-ops-api.constants.ts:53-54 |
| E-03 | API (Ops, GHI) | PUT {env}/sapp-26297/classes/:classId/students/transfer → Ops PUT /api/v1/classes/:id/students/transfer | sapp-26297.controller.ts:128,151 · sapp-ops-api.constants.ts:44-45 |
| E-04 | API (Ops, đọc) | GET {env}/sapp-26297/classes/:classId/students/:studentId/transfer-record | sapp-26297.controller.ts:174,200 · sapp-ops-api.constants.ts:59-60 |
| E-05 | API (Ops, GHI) | PUT {env}/sapp-26297/classes/:classId/students/:studentId/revert-transfer (không body) | sapp-26297.controller.ts:228,254 · sapp-ops-api.constants.ts:62-63 |
| E-06 | API (Ops, đọc) | GET {env}/sapp-26297/classes/:classId/students | sapp-26297.controller.ts:282,302 · sapp-ops-api.constants.ts:21 |
| E-07 | API (Ops, GHI – xóa mềm) | DELETE {env}/sapp-26297/classes/student (có body) → Ops DELETE /api/v1/classes/student | sapp-26297.controller.ts:324,342 · sapp-ops-api.constants.ts:71 |
| E-08 | API (LMS, đọc) | GET {env}/sapp-26297/lms/courses + header X-Lms-Token → LMS GET <SAPP_LMS_API_URL_*>/courses | sapp-26297.controller.ts:360,379 · be/libs/constants/src/lib/sapp/sapp-26297.constants.ts:11-25 |
| E-09 | API (HubSpot, GHI) | POST {env}/sapp-26297/hubspot/contacts | sapp-26297.controller.ts:399,418 |
| E-10 | API (HubSpot, đọc) | GET {env}/sapp-26297/hubspot/contacts/:contactId | sapp-26297.controller.ts:437,451 |
| E-11 | API (HubSpot, GHI) | POST {env}/sapp-26297/hubspot/tickets | sapp-26297.controller.ts:465,485 |
| E-12 | API (HubSpot, GHI – kích hoạt đồng bộ) | PATCH {env}/sapp-26297/hubspot/tickets/:ticketId | sapp-26297.controller.ts:505,526 |
| E-13 | API (HubSpot, đọc) | GET {env}/sapp-26297/hubspot/tickets/:ticketId | sapp-26297.controller.ts:547,561 |
| E-14 | API (HubSpot, đọc) | GET {env}/sapp-26297/hubspot/ticket-pipelines | sapp-26297.controller.ts:575,589 |
| E-15 | API (HubSpot, đọc) | GET {env}/sapp-26297/hubspot/owners | sapp-26297.controller.ts:603,617 |
| E-16 | API dùng chung (→ nền tảng) | GET {env}/sapp-ops/sql/status · POST {env}/sapp-ops/sql/run — 26297 gọi với id s26297.* | be/apps/api/src/sapp/sapp-ops.controller.ts:2404,2416,2430,2442 · be/libs/constants/src/lib/sapp/sapp-ops-sql.constants.ts:29-32 |
| E-17 | Seed dữ liệu | Migration đăng ký tool sapp.011 trong bảng tools, nhóm sapp | be/libs/database/migrations/1786300000000-seed-tool-sapp-26297.ts:13-19 · fe/src/data/constants/tool-codes.ts:42 |
Bảng URL FE ↔ route: fe/src/data/constants/api.ts:339-376; hàm gọi: fe/src/data/services/sapp/sapp-26297.service.ts:28-178.
3. Hành động người dùng / hệ thống ​
"Tester" = người dùng màn tool; role cụ thể chưa xác nhận (route đều @Public(), màn chỉ bị chặn bởi ToolGate → nền tảng) — xem Q-01.
3.1 Hành động chung của màn ​
| ID | Ai (role) | Làm gì | Điểm vào | Kết quả quan sát được | Bằng chứng |
|---|---|---|---|---|---|
| A-01 | Tester | Mở màn tool | E-01 | Màn chỉ hiện nếu tool sapp.011 được API biết và mở cho viewer (→ nền tảng ToolGate). Tiêu đề "SAPP-26297 Chuyển nhượng học viên", mô tả "Test trên nhánh sapp-sprint-49 · luồng 1–6 của guide QA" | fe/src/ui/pages/works/sapp/tools/shell.tsx:427-435 · sapp-26297-tool.tsx:205-220 · 1786300000000-seed-tool-sapp-26297.ts:6-8 |
| A-02 | Tester | Chọn môi trường dev hoặc staging (mặc định dev) | E-01 | Mọi bước sau gọi route của môi trường đó; không có lựa chọn production | sapp-26297-tool.tsx:179,222-234 · sapp-26297.service.ts:16 |
| A-03 | Tester | Đọc cảnh báo đỏ về thao tác GHI | E-01 | Banner: mọi nút "Chạy (GHI)" sửa thẳng dữ liệu Ops và HubSpot của môi trường đang chọn; dùng học viên test riêng; không có route production | sapp-26297-tool.tsx:238-244 |
| A-04 | Tester | Nhập fixture (id dùng chung cho mọi bước: lớp L1/L2/F1, subject, học viên A/B/C, nhóm 3B, token LMS, id HubSpot, mã lớp…) | E-01 | Ô hiển thị theo tab: tab Chuẩn bị hiện tất cả; ô không gắn luồng thuộc luồng 1–4; ô gắn luồng chỉ hiện ở luồng đó. Giá trị lưu trình duyệt, trừ token | sapp-26297-flows.ts:36-191 · sapp-26297-tool.tsx:196-201,246-269 |
| A-05 | Tester | Chuyển tab luồng | E-01 | Hiện tóm tắt luồng + danh sách case (đóng, bấm "Mở" để xem bước) | sapp-26297-tool.tsx:271-304,120-176 |
| A-06 | Tester | Mở một case | E-01 | Hiện "Kỳ vọng", cảnh báo nổi bật (nếu case có highlight), rồi lần lượt các bước | sapp-26297-tool.tsx:131-173 |
| A-07 | Tester | Ở bước gọi API: xem/sửa ô field (điền sẵn từ fixture), xem preview request Ops/LMS/HubSpot, bấm "Chạy" / "Chạy (GHI)" | E-02…E-15 | Nút bị khóa khi còn ô chứa {token} chưa điền ("Chưa điền: …"). Ô để trống = không gửi field đó. Thao tác GHI hỏi xác nhận window.confirm kèm preview, không có undo | sapp-26297-step-call.tsx:422-441,486-547 · sapp-26297-flows.ts:212-219 |
| A-08 | Hệ thống (FE) | Tự chấm kết quả theo danh sách check của bước | — | Mỗi check một dòng ✓/✗ + giá trị thực; tổng "x/y đạt"; response thô trong khối "Response Ops — HTTP n" | sapp-26297-step-call.tsx:251-395,461-483,555-583 |
| A-09 | Hệ thống (FE) | Ghi ngược giá trị từ response vào fixture (produces) | — | contact_id ← contact.id, ticket_id ← id (HubSpot); hs_user_id ← cột user_id (SQL) | sapp-26297-step-call.tsx:449-458,532-539 · sapp-26297-flows.ts:1550,1592,1636 |
| A-10 | Tester | Ở bước SQL: xem câu SQL, nhập param (nếu có), bấm chạy | E-16 | Chạy câu BE cùng id trên DB Ops dev/staging; câu GHI hỏi xác nhận (→ nền tảng FlowStepSql) | sapp-26297-tool.tsx:96-107 · fe/src/ui/components/works/sapp/sapp-26878-step-sql.tsx:203-213 |
| A-11 | Tester | Ở bước hướng dẫn: đọc danh sách thao tác tay | — | Chỉ hiển thị chữ, không gọi gì | sapp-26297-tool.tsx:109-117 |
| A-12 | Hệ thống (FE) | Khi gọi thất bại (API của mình trả lỗi) | — | Hộp đỏ "Không gọi được Ops qua API: <status> <code> <message>" | sapp-26297-step-call.tsx:178-183,549-553 |
3.2 Hành động theo từng thao tác API (bước "call") ​
| ID | Ai | Làm gì | Điểm vào | Kết quả quan sát được | Bằng chứng |
|---|---|---|---|---|---|
| A-20 | Tester | Lấy danh sách ứng viên nhận chuyển nhượng cho người cho A tại L1 (transfer_from, page_index, page_size, text, filter_class_id, sort_type) | E-02 | Trả nguyên văn Ops; mảng ứng viên ở data.data, mỗi ứng viên có eligible_classes; phân trang ở data.metadata | sapp-26297-flows.ts:606-647 · sapp-26297.common.service.ts:105-117 |
| A-21 | Tester | Chuyển nhượng suất của A tại L1 cho B học tại L2 (transfer_from, transfer_to, transfer_to_class_id) | E-03 | GHI trên Ops: A → TRANSFERED_TO, B → BE_TRANSFERRED (theo mô tả route); Ops trả thành công hoặc mã từ chối, tool hiển thị nguyên văn | sapp-26297-flows.ts:358-391 · sapp-26297.controller.ts:134-138 · sapp-26297.common.service.ts:119-128 |
| A-22 | Tester | Đọc bản ghi chuyển nhượng hiệu lực của người cho (dữ liệu popup hủy) | E-04 | Có bản ghi: record_id, transferred_at, performed_by, transfer_from, transfer_to, transfer_to_class, transfer_to_progress. Không có: HTTP 200 + data: null | sapp-26297-flows.ts:421-434,879-896,945-953 · sapp-26297.controller.ts:181-185 |
| A-23 | Tester | Hủy chuyển nhượng (class_id = L1, student_id = người CHO) | E-05 | GHI: trạng thái trả lại nơi còn do chuyển nhượng đặt, bản ghi → REVERTED (theo mô tả route) | sapp-26297-flows.ts:405-419 · sapp-26297.controller.ts:235-239 |
| A-24 | Tester | Đọc danh sách học viên lớp (trang 1, 50 dòng) để xem cờ nút can_transfer / can_revert_transfer | E-06 | Hàng ở data.students, phân trang meta | sapp-26297-flows.ts:436-450 · sapp-26297.common.service.ts:148-165 |
| A-25 | Tester | Xóa học viên khỏi lớp (user = danh sách id cách dấu phẩy, classId, reason = "test SAPP-26297") | E-07 | GHI: xóa mềm mọi học viên Ops không đưa vào skipped[]; response có removed[], skipped[], invalid_student[] | sapp-26297-flows.ts:452-472 · sapp-26297-step-call.tsx:118-129 · sapp-26297.common.service.ts:167-176 |
| A-26 | Tester | Lấy "Khóa học của tôi" trên LMS bằng token HỌC VIÊN (page_index, page_size, tùy chọn name/type/status) | E-08 | Trả nguyên văn LMS; token đi qua header X-Lms-Token, không lưu | sapp-26297-flows.ts:1334-1357 · sapp-26297.service.ts:100-115 · sapp-26297.common.service.ts:188-230 |
| A-27 | Tester | Tạo contact HubSpot (email, firstname="Test", lastname="SAPP-26297", lop_dang_ky) | E-09 | Tạo mới hoặc dùng lại contact cùng email và cập nhật nó bằng các property gửi lên; trả { contact, reused }; tool ghi contact_id | sapp-26297-flows.ts:1530-1551 · be/libs/modules/src/lib/hubspot/common.service.ts:622-661 |
| A-28 | Tester | Tạo ticket HubSpot gắn contact (subject = "SAPP-26297 test {today}", pipeline, stage MỞ, owner, phan_loai_yeu_cau_dich_vu mặc định "Bảo lưu khoá học", lop_dang_ki_chinh, lop_moi, khu_vuc) | E-11 | Ticket tạo ở stage mở, gắn contact (association type 16); tool ghi ticket_id | sapp-26297-flows.ts:1553-1593 · hubspot/common.service.ts:567-601 · be/libs/constants/src/lib/hubspot/hubspot.constants.ts:57 |
| A-29 | Tester | Kéo ticket sang Close success (PATCH hs_pipeline_stage) | E-12 | GHI; theo ghi chú: kích hoạt webhook → Kafka hubspot_enrollment → đồng bộ thật phía SAPP | sapp-26297-flows.ts:1595-1613 · sapp-26297.controller.ts:508-511 |
| A-30 | Tester | Sửa property ticket (mã lớp, kéo ra stage mở) | E-12 | GHI HubSpot | sapp-26297-flows.ts:1662-1673,1716-1741 |
| A-31 | Tester | Đọc ticket (property đồng bộ + contact gắn) | E-13 | Chấm properties.trang_thai_dong_bo_lms = SYNC_SUCCESS / SYNC_FAILURE; trống = đồng bộ chưa xong | sapp-26297-flows.ts:1615-1630 · sapp-26297.common.service.ts:270-278 |
| A-32 | Tester | Lấy pipeline ticket + stage và danh sách owner để chép id vào fixture | E-14, E-15 | Không có check, không tự ghi fixture — tester chép tay | sapp-26297-flows.ts:1512-1528 |
3.3 Danh mục luồng và case (từ sapp-26297-flows.ts) ​
Ký hiệu bước: C:op = gọi API tự chấm · S:prefix = panel SQL · G = hướng dẫn tay. "Kỳ vọng" là văn bản tool hiển thị (lấy từ guide QA, không phải rule code mind_keep).
| Luồng / Case | Tên | Bước | Kỳ vọng (tool chấm/hiển thị) | Bằng chứng |
|---|---|---|---|---|
| Chuẩn bị · Dọn | Chạy trước mọi luồng, chỉ DB dev/staging | sapp-26297-flows.ts:538-592 | ||
| 0.1 | Kiểm tiền điều kiện | S:pre (5 câu đọc) | Đủ 3 cột migration · L1/L2 LESSON cùng subject · A, B NORMAL, 0% · B chưa nhận chuyển nhượng môn này | sapp-26297-flows.ts:544-573 |
| 0.2 | Dọn dữ liệu sau test | S:clean (3 câu GHI) · G | A,B,C về NORMAL · tiến độ về 0 · xóa bản ghi lịch sử test; khôi phục xóa mềm nhóm B bằng SQL #3 panel 3B | sapp-26297-flows.ts:574-590 |
| 1 · Chuyển nhượng | AC1,2,4,5,6,8,9 | sapp-26297-flows.ts:594-865 | ||
| 1.A | Happy path | C:candidates · G biến thể · C:transfer · S:f1v · C:classStudents ×3 (L1, L2, F1) · G Kafka | B có trong ứng viên với L2 (progress 0), A không có; transfer success=true, data.message="Update successfully"; cờ: A@L1 can_transfer=false, can_revert=true; B@L2 cả hai false; A@F1 can_revert=false | sapp-26297-flows.ts:602-705 |
| 1.1 | Lớp nhận = L1 | G · C:transfer (transfer_to_class_id=L1) | Thành công | sapp-26297-flows.ts:707-719 |
| 1.2 | A đã có tiến độ tại L1 | S:c12 (GHI 3/10) · C:transfer · S:int | 400|100332 | sapp-26297-flows.ts:721-732 |
| 1.3 | B đã có tiến độ tại L2 | S:c33 · C:transfer · S:int | 400|100332 | sapp-26297-flows.ts:734-745 |
| 1.4 | B đang RESERVED tại L2 | S:c34 · C:transfer · S:int | 400|100332 | sapp-26297-flows.ts:747-758 |
| 1.5 | A tại F1 đang RESERVED | S:c36 · C:transfer · S:int | 400|100333 + đủ 5 replacements | sapp-26297-flows.ts:760-783 |
| 1.6 | A không có mặt tại F1 | G · C:transfer | Thành công | sapp-26297-flows.ts:785-795 |
| 1.7 / 1.8 / 1.9 | Trỏ sang 3.5 / 3.1 / 3.2 | G | 100332 / 100331 / 100331 | sapp-26297-flows.ts:797-813 |
| 1.10 | transfer_from == transfer_to | C:transfer (A→A) | Chỉ check success=false — mã chưa chốt | sapp-26297-flows.ts:815-826 |
| 1.10b | Thiếu transfer_to_class_id | C:transfer (ô trống) | HTTP Ops 422 | sapp-26297-flows.ts:828-840 |
| 1.11 | Chuyển nhượng lại sau hủy | G → 2.B | Thành công | sapp-26297-flows.ts:842-846 |
| AC9 | Kiểm lại lúc confirm | G (thao tác UI Ops) · C:transfer | 400|100332 | sapp-26297-flows.ts:848-863 |
| 2 · Hủy chuyển nhượng | AC10, AC12; tiền đề 1.A đã chạy | sapp-26297-flows.ts:867-1030 | ||
| 2.A | Happy path | C:transferRecord · C:revertTransfer · S:f2v · C:classStudents ×2 | Record đủ field · hủy OK · A@L1 can_transfer=true, can_revert=false · B@L2 can_revert=false | sapp-26297-flows.ts:874-922 |
| 2.B | Chuyển nhượng lại (AC12) | C:transfer · S:f1v · G | Thành công, vẫn 1 dòng lịch sử | sapp-26297-flows.ts:924-933 |
| 2.1 | Hủy 2 lần | C:revert ×2 | Lần 2 400|100337 | sapp-26297-flows.ts:936-942 |
| 2.2 | GET record khi không còn hiệu lực | C:transferRecord | HTTP 200 + data: null | sapp-26297-flows.ts:945-953 |
| 2.3 | GET record với student = B | C:transferRecord | data: null | sapp-26297-flows.ts:956-965 |
| 2.4 | L2 đã bị xóa | G · S:c24 (GHI xóa mềm lớp + khôi phục) · C:revert | Vẫn hủy thành công phía A | sapp-26297-flows.ts:968-981 |
| 2.5 | B đã bị xóa khỏi L2 | G · S:c25 · C:revert | Vẫn hủy thành công | sapp-26297-flows.ts:984-993 |
| 2.6 | B đã học tại L2 | G · S:c26 · C:transferRecord · C:revert · S:f2v | transfer_to_progress có giá trị · hủy OK · tiến độ B giữ | sapp-26297-flows.ts:996-1011 |
| 2.7 | F1 của A bị bảo lưu sau chuyển nhượng | G · S:c36 · C:revert · S:f2v | Hủy OK, F1 giữ RESERVED (highlight "dễ báo nhầm bug nhất") | sapp-26297-flows.ts:1014-1027 |
| 3A · Chặn chuyển nhượng | Sau mỗi case chạy 3.7 (S:int) | sapp-26297-flows.ts:1032-1161 | ||
| 3.1 | Lớp nhận khác môn | S:c31 · C:transfer (tester điền lớp) · S:int | 400|100331 transfer_receiving_class_invalid | sapp-26297-flows.ts:1039-1057 |
| 3.2 | Lớp nhận là Revision | S:c32 · C:transfer · S:int | 400|100331 | sapp-26297-flows.ts:1059-1074 |
| 3.3 / 3.4 | Người nhận có tiến độ / không NORMAL | S:c33 / S:c34 · C:transfer · S:int | 400|100332 | sapp-26297-flows.ts:1076-1100 |
| 3.5 | Người nhận đã nhận chuyển nhượng môn này (AC8) | C:transfer A→B · S:c35 · C:transfer C→B · C:revert · C:transfer C→B | C→B lần 1 100332; sau hủy A→B thì C→B thành công | sapp-26297-flows.ts:1102-1121 |
| 3.6 | Lớp đi kèm sai trạng thái (AC6) | S:c36 · C:transfer · G câu FS · S:int | 400|100333, đủ 5 replacements, replacements.status=RESERVED; biến thể Revision dựng tay | sapp-26297-flows.ts:1123-1159 |
| 3B · Chặn xóa học viên | Test trên sapp-sprint-49; 8 trạng thái bị chặn; xóa MỀM thật | sapp-26297-flows.ts:1163-1271 | ||
| 3.8 / 3.9 / 3.12 / 3.13 / 3.14 | Xóa 1 học viên TRANSFERED_TO / BE_TRANSFERRED / RESERVED / RETAKING / REASSIGNED | S:grpb (dựng type) · C:removeStudents | 400|490037 class_user_cannot_be_removed; 3.13, 3.14 ghi "ĐỔI so với production" | sapp-26297-flows.ts:1166-1180,1187-1188,1226-1228 |
| 3.10 | Bulk: 1 chuyển nhượng + 1 NORMAL | S:grpb · C:removeStudents · G verify DB | HTTP 200, user_x trong skipped, user_y không (highlight "quan trọng nhất luồng 3") | sapp-26297-flows.ts:1190-1214 |
| 3.11 | Bulk: tất cả bị chặn | S:grpb · C:removeStudents | 400|490037 | sapp-26297-flows.ts:1216-1225 |
| 3.15 | Xóa 1 học viên NORMAL | S:grpb · C:removeStudents · G khôi phục | 200, removed có user_x, skipped rỗng | sapp-26297-flows.ts:1231-1242 |
| 3.16 | Học viên đã ở lớp Lesson khác | G · C:removeStudents | 200 + invalid_student không rỗng (định danh HỌ TÊN) | sapp-26297-flows.ts:1245-1255 |
| UI | Checkbox trên UI Ops bị disable | G | Kiểm tay; API không trả can_delete | sapp-26297-flows.ts:1257-1269 |
| 3C · Chặn hủy chuyển nhượng | sapp-26297-flows.ts:1273-1330 | |||
| 3.17 | Hủy hai lần | G · C:revert ×2 | Lần 2 400|100337 | sapp-26297-flows.ts:1279-1287 |
| 3.18 / 3.19 | Hủy cho C (chưa chuyển nhượng) / cho B (người nhận) | C:revert | 400|100337 | sapp-26297-flows.ts:1290-1311 |
| 3.20 | class_id không phải Lesson (F1) | C:revert · G | Chỉ check success=false — mã CLASS_NOT_FOUND chưa chốt dạng | sapp-26297-flows.ts:1314-1328 |
| 4 · LMS | SAPP-28974; cần token HỌC VIÊN; route trả 13058 tới khi có key URL | sapp-26297-flows.ts:1402-1508 | ||
| 4.A | Trước chuyển nhượng — mốc | G · C:lmsCourses (token A) | Body chứa L1 | sapp-26297-flows.ts:1409-1419 |
| 4.B | Sau chuyển nhượng — người CHO | G · C:lmsCourses (A) · G UI | Body KHÔNG chứa L1 và F1 | sapp-26297-flows.ts:1421-1439 |
| 4.C | Sau chuyển nhượng — người NHẬN | C:lmsCourses (B) · G UI | Body chứa L2 (highlight) | sapp-26297-flows.ts:1441-1453 |
| 4.D | Sau hủy | G · C:lmsCourses A · C:lmsCourses B · S:f2v | A thấy lại L1, B vẫn thấy L2 | sapp-26297-flows.ts:1455-1465 |
| 4.1–4.8 | Hồi quy theo type: RESERVED, RETOOK, MOVED_OUT → ẩn; CANCELED, REASSIGNED, RETAKING, MOVED_IN, WITHDRAWN → hiện | S:lms (đọc / GHI đổi type / GHI trả NORMAL) · C:lmsCourses (token hồi quy) | Lớp test ẩn/hiện đúng; 4.4 "BẮT BUỘC chạy"; 4.8 "chưa rõ đúng ý nghiệp vụ" | sapp-26297-flows.ts:1369-1400,1466-1483 |
| 4.F | Filter cũ vẫn chạy | C:lmsCourses (+ name/type/status) · G | Không có check tự động; ghi chú "ngoài phạm vi: vào thẳng nội dung bằng URL" | sapp-26297-flows.ts:1485-1506 |
| 5 · HubSpot đồng bộ lại | SAPP-28978; end-to-end BLOCKED bởi guard "Ticket đã được xử lý" | sapp-26297-flows.ts:1680-1817 | ||
| 5.0 | Chuẩn bị HubSpot | C:hsTicketPipelines · C:hsOwners · C:hsCreateContact | Có pipeline/stage/owner trong fixture · contact có lop_dang_ky = MA_LOP_CU | sapp-26297-flows.ts:1686-1696 |
| A.1 | Đồng bộ lần đầu (Bảo lưu) | C:hsCreateTicket · C:hsUpdateTicket (Close) · C:hsGetTicket · S:hs5 | Ticket SYNC_SUCCESS; 3 dòng enrollment (RESERVED / REASSIGNED ×2) — dòng DB kiểm bằng mắt | sapp-26297-flows.ts:1698-1708 |
| B | Xác nhận blocker (đồng bộ lần 2) | C:hsUpdateTicket (bỏ MA_LOP_MOI_1) · C:hsUpdateTicket (ra stage mở) · C:hsUpdateTicket (Close) · C:hsGetTicket · S:hs5 · G | Hiện tại: DB không đổi + log "Ticket đã được xử lý" | sapp-26297-flows.ts:1710-1751 |
| 5.1–5.8 | Hoàn tác khi xóa mã lớp: bỏ MA_LOP_CU; bỏ lớp chưa khai giảng; bỏ lớp đã khai giảng có tiến độ (S:hs5s); học viên đã đổi trạng thái; mã chưa có trên LMS; học viên TRANSFERED_TO; xóa toàn bộ; 1 ok 1 lỗi | [S:hs5s] · G · C:hsUpdateTicket (sửa mã) · C:hsUpdateTicket (Close) · C:hsGetTicket · S:hs5 | Mọi case gắn highlight BLOCKED; kết quả DB/mail kiểm bằng mắt | sapp-26297-flows.ts:1645-1678,1752-1815 |
| 6 · Mail | SAPP-28980; mail kiểm tay trong hộp thư dev | sapp-26297-flows.ts:1826-1962 | ||
| 6.1 | E01 — mã lớp không khớp contact | C:hsCreateContact (lop_dang_ky = MA_LOP_MOI_1) · C:hsCreateTicket · C:hsUpdateTicket · C:hsGetTicket · G checklist mail | Ticket SYNC_FAILURE; mail kiểm tay | sapp-26297-flows.ts:1833-1850 |
| 6.2 | E01 khối mã lớp rỗng | G | Kiểm tay | sapp-26297-flows.ts:1852-1863 |
| 6.3 | E04 — không tạo được tài khoản | G · C:hsCreateContact · C:hsCreateTicket · C:hsUpdateTicket · C:hsGetTicket · G | SYNC_FAILURE | sapp-26297-flows.ts:1865-1881 |
| 6.4 | E03 — 2 mã cùng lỗi | G · C ×4 như trên · G | SYNC_FAILURE; 2 lỗi gộp 1 mail (kiểm tay) | sapp-26297-flows.ts:1883-1898 |
| 6.5 / 6.6 / 6.7 | E03 hoàn tác / E05 theo khu_vuc / E05 fallback | G | BLOCKED (phụ thuộc luồng 5) | sapp-26297-flows.ts:1900-1934 |
| 6.8 | Mail lỗi không làm hỏng đồng bộ | G | "Không dựng được từ tool" | sapp-26297-flows.ts:1936-1945 |
| 6.T | Đối chiếu placeholder template | G | Danh sách placeholder theo template | sapp-26297-flows.ts:1947-1960 |
Tổng: 9 luồng (tab) — Chuẩn bị 2 · 1: 14 · 2: 9 · 3A: 6 · 3B: 10 · 3C: 4 · 4: 13 · 5: 11 · 6: 9 = 78 case (sapp-26297-flows.ts:1964-1974).
4. Dữ liệu ​
4.1 Fixture (trạng thái màn, trình duyệt) ​
| Entity / bảng | Field | Type | Bắt buộc | Mặc định / enum | Bằng chứng |
|---|---|---|---|---|---|
Fixture (localStorage sapp-26297:fixture) | class_id_l1, class_id_l2, foundation_class_id, subject_id, user_a, user_b, user_c | string | Theo bước dùng tới | Luồng 1–4 (không gắn flows) | sapp-26297-flows.ts:36-63 · sapp-26297-tool.tsx:25 |
| Fixture | class_id, user_x, user_y | string | Theo bước | Chỉ luồng 3B; user_y trống ⇒ dùng user_x | sapp-26297-flows.ts:64-81 · sapp-26297-tool.tsx:69 |
| Fixture | lms_token_a, lms_token_b, lms_token | string (password) | Luồng 4 | secret — không lưu trình duyệt | sapp-26297-flows.ts:84-104 · sapp-26297-tool.tsx:43-56 |
| Fixture | lms_user_id, lms_class_id | string | Luồng 4 hồi quy | — | sapp-26297-flows.ts:105-116 |
| Fixture | hs_pipeline_id, hs_stage_open_id, hs_stage_close_id, hs_owner_id, khu_vuc, contact_email, class_code_old, class_code_1, class_code_2 | string | Luồng 5–6 | khu_vuc: từ thứ 2 là ACCA/CFA/CMA/Cert/Dip/CGMA (gợi ý) | sapp-26297-flows.ts:119-172 |
| Fixture | contact_id, ticket_id, hs_user_id | string | Luồng 5–6 | Tự điền khi chạy bước | sapp-26297-flows.ts:173-190 |
| Token template dựng sẵn | {today} (YYYY-MM-DD giờ VN) … | string | — | → nền tảng fillTemplate | fe/src/ui/components/works/sapp/sapp-26878-step-ops.tsx:311-324 |
4.2 Payload route (DTO — mọi field đều tùy chọn) ​
| Entity / bảng | Field | Type | Bắt buộc | Mặc định / enum | Bằng chứng |
|---|---|---|---|---|---|
| Body transfer | transfer_from, transfer_to, transfer_to_class_id | string | Không (Ops tự validate) | — | be/libs/interface/src/lib/dto/sapp/sapp-26297.dto.ts:16-32 |
| Body remove | user[], classId, reason | string[] / string | Không | FE mặc định reason = "test SAPP-26297" | sapp-26297.dto.ts:34-45 · sapp-26297-flows.ts:469 |
| Query candidates | transfer_from, page_index, page_size, text, filter_class_id, sort_type | string | Không | sort_type: LATEST (mặc định phía Ops) / A_Z; FE mặc định page 1 / size 20 | sapp-26297.dto.ts:47-70 · sapp-26297-flows.ts:611-618 |
| Query students | page_index, page_size | string | Không | FE: 1 / 50 | sapp-26297.dto.ts:72-78 · sapp-26297-flows.ts:446-447 |
| Query LMS courses | page_index, page_size, name, type, status | string | Không | FE: 1 / 50 | sapp-26297.dto.ts:93-110 · sapp-26297-flows.ts:1352-1353 |
| Header LMS | X-Lms-Token | string | Có (thiếu ⇒ 13059) | Tiền tố Bearer bị bỏ | sapp-26297.controller.ts:369 · sapp-26297.common.service.ts:205-209 |
| Body HubSpot | properties (tên property nội bộ → giá trị), contactId (ticket) | object / string | Không | — | sapp-26297.dto.ts:112-127 |
| Response Ops/LMS | upstreamStatus, body (envelope nguyên văn) | number / unknown | — | — | sapp-26297.dto.ts:80-91 · sapp-26297.common.service.ts:23-26 |
4.3 Property HubSpot tool đọc ​
| Entity / bảng | Field | Type | Bắt buộc | Mặc định / enum | Bằng chứng |
|---|---|---|---|---|---|
| HubSpot ticket | subject, hs_pipeline, hs_pipeline_stage, hubspot_owner_id, lop_dang_ki_chinh, lop_moi, phan_loai_yeu_cau_dich_vu, khu_vuc, trang_thai_dong_bo_lms (+ contact gắn) | string | — | lop_dang_ki_chinh/lop_moi tách dấu phẩy; trang_thai_dong_bo_lms = SYNC_SUCCESS/SYNC_FAILURE | sapp-26297.constants.ts:27-43 · sapp-26297.common.service.ts:270-278 |
| HubSpot contact | email, firstname, lastname, lop_dang_ky, l_p_b_o_l_u_h_c_l_i | string | — | lop_dang_ky tách dấu chấm phẩy | sapp-26297.constants.ts:45-51 |
4.4 Bảng Ops DB mà catalogue SQL s26297.* đọc / ghi ​
| Entity / bảng | Field | Type | Bắt buộc | Mặc định / enum | Bằng chứng |
|---|---|---|---|---|---|
class_user_instances | type (GHI: dựng RESERVED, MOVED_OUT, TRANSFERED_TO, {target_type}, trả NORMAL) | text | — | Chuỗi lưu thật: TRANSFERED_TO (một R), BE_TRANSFERRED | be/libs/constants/src/lib/sapp/sapp-26297-sql.constants.ts:13-14,172-177,218-222,231-236,260-269,304-315 |
class_user_instances | learning_progress (GHI tiến độ 3/10, 1/10, 2/10, về 0) | json | — | — | sapp-26297-sql.constants.ts:112-117,142-147,164-170,238-244,297-303 |
class_user_instances | deleted_at (GHI xóa mềm / khôi phục) | timestamp | — | — | sapp-26297-sql.constants.ts:129-139,223-228 |
classes | deleted_at (GHI xóa mềm / khôi phục L2) | timestamp | — | — | sapp-26297-sql.constants.ts:119-127 |
class_user_change_history_instances | subject_id, cancelled_by, cancelled_at, status, transferred_user_id… (đọc); DELETE bản ghi A→B, C→B khi dọn | — | — | status: TRANSFERRED / REVERTED (theo ghi chú) | sapp-26297-sql.constants.ts:23-27,75-90,104-109,182-185,245-249 |
classes_foundation_classes | normal_class_id → foundation_class_id (đọc) | — | — | — | sapp-26297-sql.constants.ts:57-60,190-193 |
user_hubspot_enrollments | class_code_target, class_action_type, user_id, sync_status_hubspot, deleted_at (đọc) | — | — | — | sapp-26297-sql.constants.ts:272-279 |
| Tổng catalogue | 39 entry (20 đọc, 19 ghi); bản FE giống hệt bản BE tại e77c47a (so tự động id + writes + text) | — | — | — | sapp-26297-sql.constants.ts:20-316 · fe/src/ui/components/works/sapp/sapp-26297-sql.ts:9-270 |
4.5 Cấu hình & seed ​
| Entity / bảng | Field | Type | Bắt buộc | Mặc định / enum | Bằng chứng |
|---|---|---|---|---|---|
tools (DB mind_keep) | code=sapp.011, name="SAPP 26297: Chuyển nhượng học viên", nhóm sapp | — | Có (thiếu ⇒ màn bị khóa) | ON CONFLICT DO NOTHING | 1786300000000-seed-tool-sapp-26297.ts:13-19 |
env_systems | SAPP_LMS_API_URL_DEV, SAPP_LMS_API_URL_STAGING (URL gồm prefix LMS; dấu / cuối bị cắt) | string | Có cho luồng 4 | Thiếu ⇒ 13058 | be/libs/constants/src/lib/env-system/env-system.constants.ts:39-40 · sapp-26297.common.service.ts:180-186 |
env_systems | Credentials Ops (SAPP_OPS_*) — → nền tảng auto-login | — | — | — | sapp-26297.common.service.ts:62-73 |
5. Rule & validation tìm thấy ​
| ID | Rule (ngôn ngữ nghiệp vụ) | Loại | Áp dụng ở | Bằng chứng | Nghi là bug? |
|---|---|---|---|---|---|
| R-01 | Với thao tác Ops và LMS, mọi câu trả lời của hệ thống đích — kể cả từ chối (4xx) — được trả về nguyên văn dưới dạng dữ liệu (HTTP 200 của mind_keep, kèm mã HTTP đích và body) để tool chấm được đúng mã lỗi | điều kiện | A-20…A-26 | sapp-26297.controller.ts:31-41,65-76 · sapp-26297.common.service.ts:30-40,90-93 | Không |
| R-02 | Chỉ khi không gọi được hệ thống đích (mạng, timeout) mới báo lỗi 13042 "Failed to call SAPP Ops API" kèm lý do | điều kiện | A-20…A-26 | sapp-26297.common.service.ts:75-88,217-225 · be/libs/constants/src/lib/error-codes/error-codes.constants.ts:750-754 | Không |
| R-03 | Mind_keep không kiểm tra field nào của payload Ops; mọi field tùy chọn để request sai cố ý tới được Ops (vd thiếu transfer_to_class_id ⇒ Ops 422) | validation | A-20…A-25 | sapp-26297.dto.ts:8-14 | Không |
| R-04 | Giá trị query rỗng / null bị bỏ trước khi gửi (Ops thấy như "không có") | validation | A-20, A-24, A-26 | sapp-26297.common.service.ts:96-103 | Không |
| R-05 | Trên màn, ô field để trống = không gửi field đó (cách dựng case 422); field đường dẫn (class_id, student_id), header token và id bản ghi HubSpot không bao giờ vào body/query | validation | A-07 | sapp-26297-step-call.tsx:96-116 · sapp-26297-flows.ts:221-224 | ⚠️ xem R-30 |
| R-06 | Nút Chạy bị khóa khi còn ô chứa {token} chưa được fixture thay | validation | A-07 | sapp-26297-step-call.tsx:428-430,490-495,541-547 | Không |
| R-07 | 6 thao tác được coi là GHI (chuyển nhượng, hủy, xóa học viên, tạo contact, tạo ticket, sửa ticket) → nhãn "Chạy (GHI)" + hộp xác nhận kèm preview, "không có undo" | điều kiện | A-21, A-23, A-25, A-27…A-30 | sapp-26297-flows.ts:211-219 · sapp-26297-step-call.tsx:431-441,500-502 | Không |
| R-08 | Câu SQL đánh dấu GHI phải xác nhận trước khi chạy; chỉ chạy câu có sẵn trong catalogue theo id (→ nền tảng) | điều kiện | A-10 | sapp-26878-step-sql.tsx:203-213 · sapp-ops-sql.constants.ts:7-13 | Không |
| R-09 | Mọi id SQL mà một case tham chiếu phải có trong bản sao catalogue FE, thiếu thì màn lỗi khi tải | giới hạn | A-10 | sapp-26297-flows.ts:321-349 | Không |
| R-10 | Catalogue 26878 và 26297 dùng chung không gian id; trùng id ⇒ API không khởi động | giới hạn | E-16 | sapp-ops-sql.constants.ts:10-27 | Không |
| R-11 | Luồng 4: thiếu URL LMS của môi trường ⇒ lỗi 13058; thiếu token ⇒ 13059; tiền tố Bearer trong token bị bỏ | validation | A-26 | sapp-26297.common.service.ts:195-209 · error-codes.constants.ts:834-845 | Không |
| R-12 | Token LMS là token học viên, đi qua header X-Lms-Token (không phải Authorization — header đó là phiên mind_keep), chỉ dùng cho một lần gọi, không lưu | điều kiện | A-26 | sapp-26297.service.ts:100-115 · sapp-26297.common.service.ts:188-194 | Không |
| R-13 | Token đăng nhập trong fixture không bao giờ ghi xuống trình duyệt, chỉ sống tới khi đóng tab; ô nhập kiểu password | giới hạn | A-04 | sapp-26297-tool.tsx:39-56 · sapp-26297-flows.ts:29-33 | Không |
| R-14 | Fixture chỉ đưa giá trị không rỗng (đã trim) vào bước; user_y trống ⇒ dùng user_x | công thức | A-04, A-25 | sapp-26297-tool.tsx:58-71 | ⚠️ xem O-03 |
| R-15 | Thao tác HubSpot không passthrough: HubSpot từ chối ⇒ lỗi của mind_keep (vì đó là fixture hỏng, không phải kết quả cần chấm) | điều kiện | A-27…A-32 | sapp-26297.controller.ts:51-63 · sapp-26297.service.ts:117-118 | Không |
| R-16 | Tạo contact: email đã tồn tại (HubSpot 409) ⇒ dùng lại contact đó và ghi đè các property gửi lên; lỗi khác ⇒ FAILED_CREATE_HUBSPOT_OBJECT | điều kiện | A-27 | hubspot/common.service.ts:607-661 | Không |
| R-17 | Tạo ticket: gắn contact (association type 16) nếu có contactId; HubSpot từ chối ⇒ 12006 kèm câu từ chối của HubSpot | điều kiện | A-28 | sapp-26297.common.service.ts:244-254 · hubspot/common.service.ts:567-601 · hubspot.constants.ts:57 | Không |
| R-18 | lop_dang_ky (contact) tách bằng chấm phẩy; lop_dang_ki_chinh / lop_moi (ticket) tách bằng phẩy; mọi mã trên lop_dang_ki_chinh phải có trong lop_dang_ky, nếu không đồng bộ dừng với E01 (theo ghi chú code) | validation (dữ liệu) | A-27, A-28 | sapp-26297.constants.ts:30-31 · sapp-26297.common.service.ts:234-239 · sapp-26297-flows.ts:1545,1581,1586 | Không |
| R-19 | Đổi hs_pipeline_stage sang Close success của pipeline enrollment là thứ kích hoạt đồng bộ thật trên SAPP (ghi chú code; pipeline nào chưa biết) | trạng thái | A-29 | sapp-26297.common.service.ts:256 · sapp-26297-flows.ts:1600,1515 | Không |
| R-20 | "Từ chối đúng" = HTTP Ops 400 và error.code = mã mong đợi và error.message = exceptions.<khóa> | công thức | A-08 | sapp-26297-flows.ts:393-397 | Không |
| R-21 | "Cập nhật thành công" = HTTP 200, success=true, data.message="Update successfully" | công thức | A-08 | sapp-26297-flows.ts:399-403 | Không |
| R-22 | Check replacements-keys: đủ khi mỗi khóa có giá trị khác rỗng/null | công thức | 1.5, 3.6 | sapp-26297-step-call.tsx:281-293 | Không |
| R-23 | Check "ứng viên hợp lệ": người nhận có trong data.data, lớp nhận có trong eligible_classes của người đó và progress = 0 | công thức | 1.A | sapp-26297-step-call.tsx:351-370 | Không |
| R-24 | Check cờ nút: tìm học viên trong data.students của trang đang đọc (FE đọc trang 1, 50 dòng); không thấy ⇒ fail "không thấy học viên ở trang 1" | công thức / giới hạn | 1.A, 2.A | sapp-26297-step-call.tsx:381-393 · sapp-26297-flows.ts:446-447 | Không |
| R-25 | Một phần tử danh sách "là học viên X" khi nó là chuỗi = id, hoặc object có user_id/id/user.id = id (shape skipped/removed chưa đo — đoán) | công thức | 3.10, 3.15 | sapp-26297-step-call.tsx:217-224 · shared/integration/0045-be-sapp-26297-transfer-tool.md:52-54 | Không |
| R-26 | Check "lớp hiện/ẩn trên LMS" = chuỗi id lớp có/không xuất hiện ở bất kỳ đâu trong body (so khớp nguyên chuỗi) — vì chưa biết field định danh lớp | công thức | 4.A–4.D, 4.1–4.8 | sapp-26297-step-call.tsx:233-241,371-380 · sapp-26297-flows.ts:1359-1367,1413-1416 | ⚠️ xem O-04 |
| R-27 | Tool chấm theo bảng mã từ chối của Ops (từ guide QA, không phải rule do mind_keep cài): 100331 lớp nhận không hợp lệ (khác môn / Revision); 100332 điều kiện không còn hợp lệ (có tiến độ, không NORMAL, người nhận đã nhận chuyển nhượng cùng môn, đổi trạng thái lúc confirm); 100333 lớp đi kèm sai trạng thái + 5 replacements; 100337 không tìm thấy bản ghi chuyển nhượng; 490037 không xóa được học viên; 422 thiếu field bắt buộc | điều kiện (hệ đích) | Luồng 1–3 | sapp-26297-flows.ts:723-728,1041-1051,1102-1115,1125-1150,937-941,1172-1178,828-839 | Không |
| R-28 | 8 trạng thái bị chặn xóa khỏi lớp: RESERVED · RETOOK · RETAKING · MOVED_OUT · REASSIGNED · TRANSFERED_TO · BE_TRANSFERRED · WITHDRAWN; xóa được NORMAL · CANCELED · MOVED_IN (kỳ vọng theo guide) | trạng thái (hệ đích) | Luồng 3B | sapp-26297-flows.ts:1185 | Không |
| R-29 | LMS ẩn lớp khi type là RESERVED/RETOOK/MOVED_OUT (và người CHO TRANSFERED_TO); vẫn hiện CANCELED/REASSIGNED/RETAKING/MOVED_IN/WITHDRAWN và người NHẬN BE_TRANSFERRED (kỳ vọng theo guide) | trạng thái (hệ đích) | Luồng 4 | sapp-26297-flows.ts:1404-1406,1466-1483 · sapp-26297.constants.ts:19-24 | Không |
| R-30 | Ô để trống không gửi property HubSpot — nên tool không xóa được giá trị property qua ô trống, trong khi API mind_keep coi chuỗi rỗng là "xóa property" | validation | 5.1, 5.7, 6.3 | sapp-26297-step-call.tsx:110-116 · sapp-26297.dto.ts:112 · hubspot/common.service.ts:947-950 · sapp-26297-flows.ts:1757-1758,1808 | ⚠️ Observed (O-01) |
| R-31 | Không có đường production: controller chỉ khai báo route dev/… và staging/…; khóa URL LMS chỉ có dev/staging; FE chỉ cho chọn dev/staging; SQL runner không có key prod (→ nền tảng) | giới hạn | Toàn tool | sapp-26297.controller.ts:39-40 · sapp-26297.constants.ts:8-16 · sapp-26297.service.ts:16 · sapp-26297-tool.tsx:223 · api.ts:339 | Không |
| R-32 | Chuỗi so sánh trong SQL dùng giá trị lưu thật (TRANSFERED_TO, BE_TRANSFERRED), không dùng tên hằng trong code Ops | validation (dữ liệu) | A-10 | sapp-26297-flows.ts:10-11 · sapp-26297-sql.constants.ts:13-14 | Không |
| R-33 | Chỉ khi có đủ row sapp.011 thì màn mở (ToolGate fail-closed → nền tảng) | điều kiện | A-01 | 1786300000000-seed-tool-sapp-26297.ts:6-8 | Không |
Trạng thái học viên trong lớp mà tool dựng / chấm (theo mô tả route và kỳ vọng guide — máy trạng thái thuộc Ops, không cài ở mind_keep):
text
A@L1: [NORMAL, 0%] --(PUT transfer, Ops chấp nhận)--> [TRANSFERED_TO] ; lớp đi kèm của A cũng đổi (cascade)
B@L2: [NORMAL, 0%] --(PUT transfer, Ops chấp nhận)--> [BE_TRANSFERRED]
Bản ghi lịch sử A→B: (không có) --(transfer)--> [TRANSFERRED] --(revert-transfer)--> [REVERTED] --(transfer lại)--> [TRANSFERRED] (tái dùng bản ghi, vẫn 1 dòng)
A@L1: [TRANSFERED_TO] --(revert)--> [NORMAL] (status tính lại theo tiến độ) ; lớp đi kèm đã bị nghiệp vụ khác đổi (vd RESERVED) thì GIỮ NGUYÊN
Ticket HubSpot: [stage mở] --(PATCH stage = Close success)--> đồng bộ SAPP --> trang_thai_dong_bo_lms = SYNC_SUCCESS | SYNC_FAILUREBằng chứng: sapp-26297.controller.ts:137,160,238,266 · sapp-26297-flows.ts:482-503,924-932,1014-1019,1595-1630.
6. Quyền, tích hợp, thông báo, job ​
| Loại | Chi tiết | Bằng chứng |
|---|---|---|
| Quyền (API) | Cả 28 route @Public() — không cần JWT; kể cả route GHI Ops/HubSpot | sapp-26297.controller.ts:79,104,129,152,175,203,229,257,283,303,325,343,361,380,400,419,438,452,466,486,506,527,548,562,576,590,604,618 |
| Quyền (màn) | Màn nằm trong shell có ToolGate theo tool code sapp.011 (→ nền tảng). 26297 không dùng SecretGate | shell.tsx:427-435 · tool-codes.ts:42 · (không có tham chiếu SecretGate trong sapp-26297-tool.tsx) |
| Rào chắn prod | Không route prod ở controller / FE / khóa env_systems; banner cảnh báo trên màn | sapp-26297.controller.ts:39-40 · sapp-26297.constants.ts:8-16 · sapp-26297-tool.tsx:238-244 |
| Tích hợp ngoài — SAPP Ops API | 6 thao tác (3 đọc, 3 ghi) qua token staff auto-login (→ nền tảng withToken) | sapp-26297.common.service.ts:52-94,105-176 |
| Tích hợp ngoài — SAPP LMS API | GET /courses bằng token học viên; base URL từ env_systems | sapp-26297.common.service.ts:178-230 |
| Tích hợp ngoài — HubSpot | Tạo/dùng lại contact, tạo ticket gắn contact, sửa/đọc ticket, đọc contact, pipelines, owners (→ nền tảng HubSpotCommonService) | sapp-26297.common.service.ts:232-296 |
| Tích hợp ngoài — SAPP Ops DB | Qua SQL runner dùng chung, id s26297.* | sapp-ops-sql.constants.ts:29-32 · sapp-26297-tool.tsx:96-107 |
| Thông báo | mind_keep không gửi mail/push/MQTT. Mail E01/E03/E04/E05 do SAPP gửi sau đồng bộ; tool chỉ hiện checklist kiểm tay | sapp-26297-flows.ts:1819-1824,1826-1831 |
| Job / cron | Không có | (không tìm thấy tham chiếu Sapp26297 ngoài apps/api và libs/modules: be/apps/api/src/sapp/sapp.module.ts:3,9 · be/libs/modules/src/lib/sapp/module.ts:9,19,27) |
| Log | Lỗi mỗi route ghi logger.error('[handler]'); transport ghi params, không ghi header (theo comment) | sapp-26297.controller.ts:55-76 · sapp-26297.common.service.ts:191-193 |
⚠️ Observed (nghi bug / rủi ro) ​
| ID | Quan sát | Bằng chứng | Câu hỏi |
|---|---|---|---|
| O-01 | Case 5.1 ("Để trống lop_dang_ki_chinh"), 5.7 ("Để trống cả lop_dang_ki_chinh và lop_moi") yêu cầu xóa property, nhưng FE bỏ field rỗng khỏi payload ⇒ PATCH không chứa property đó ⇒ HubSpot giữ nguyên giá trị cũ; case sẽ không dựng được như mô tả. Tương tự 6.3 "để trống firstname/lastname" khi contact bị dùng lại (R-16) thì tên cũ vẫn còn | sapp-26297-step-call.tsx:110-116 · sapp-26297-flows.ts:1661-1672,1757-1758,1808,1872-1873 · hubspot/common.service.ts:947-950 | Q-02 |
| O-02 | Case 3.1 / 3.2: ô transfer_to_class_id mặc định rỗng (tester tự điền). Rỗng không bị coi là "chưa điền" nên nút Chạy không khóa; quên điền ⇒ gửi thiếu field ⇒ Ops 422 thay vì 100331 | sapp-26297-flows.ts:1049-1053,1066-1070 · sapp-26297-step-call.tsx:428-430 | Q-04 |
| O-03 | user_y trống ⇒ tự lấy user_x; case 3.10 gửi user_x,user_x và vừa đòi skipped chứa user_x vừa đòi KHÔNG chứa user_y (=user_x) ⇒ luôn có 1 check fail, dễ hiểu nhầm là Ops lỗi | sapp-26297-tool.tsx:69 · sapp-26297-flows.ts:1197-1207 | Q-05 |
| O-04 | Check "lớp biến mất" (phủ định body-contains) đạt giả nếu LMS chỉ trả mã lớp (không có class_id) hoặc trả body lỗi — code đã ghi nhận rủi ro, chờ SAPP câu 3 | sapp-26297-step-call.tsx:233-241,371-380 · sapp-26297-flows.ts:1413-1416 · shared/integration/0046-be-sapp-26297-flows-4-5-6.md:59-60 | Q-09 |
| O-05 | Toàn bộ route GHI (chuyển nhượng, hủy, xóa học viên, tạo/sửa HubSpot, kích hoạt đồng bộ) là @Public() — ai gọi được API đều sửa được dữ liệu dev/staging SAPP | sapp-26297.controller.ts:128-172,228-280,324-358,399-545 · shared/api-docs/sapp.md:2760 | Q-03 |
| O-06 | Hộp xác nhận GHI ghi "GHI dữ liệu trên Ops …" và lỗi ghi "Không gọi được Ops qua API" cho cả thao tác HubSpot/LMS — nhãn sai hệ thống | sapp-26297-step-call.tsx:437,551 | Q-15 |
| O-07 | Thao tác hsGetContact và route GET …/hubspot/contacts/:contactId tồn tại nhưng không bước nào dùng | sapp-26297-flows.ts:204 · sapp-26297-step-call.tsx:73-76,164-165 · sapp-26297.controller.ts:437-463 | Q-06 |
| O-08 | Bước "Pipeline ticket" / "Danh sách owner" không chấm và không tự ghi fixture — tester phải tự tìm trong JSON (21 pipeline dev) và chép tay | sapp-26297-flows.ts:1512-1528 | Q-06 |
| O-09 | Dọn dữ liệu s26297.clean.3 xóa cứng mọi bản ghi lịch sử A→B, C→B (không lọc deleted_at, không lọc ngày); clean.1 trả NORMAL cho A,B,C tại cả 3 lớp bất kể type gốc | sapp-26297-sql.constants.ts:231-249 | Q-05 |
7. Drift — tài liệu nói khác code ​
| ID | Tài liệu (file + section) | Tài liệu nói | Code đang làm | Bằng chứng |
|---|---|---|---|---|
| DR-01 | shared/api-docs/sapp.md §SAPP-26297 student transfer tool (dòng 2756-2758) | Tool "covers transferring a seat, reverting the transfer, and removing learners — flows 1–3" | Tool có 6 luồng (+ LMS, HubSpot, mail); 28 route. Các mục Flow 4 / Flows 5–6 phía dưới có mô tả, nhưng câu mở đầu chưa cập nhật | sapp-26297-flows.ts:1964-1974 · sapp-26297.controller.ts:360-629 |
| DR-02 | shared/api-docs/sapp.md §SAPP Ops SQL runner (dòng 2748-2750) | "every read-only s26297.* statement (16 entries, 17 statements) ran" | Catalogue hiện có 20 entry chỉ-đọc (thêm lms.1, hs5.1-3); tổng 39 entry. (0046 nói các câu đọc mới đã chạy — doc chưa cập nhật con số) | sapp-26297-sql.constants.ts:251-294 · shared/integration/0046-be-sapp-26297-flows-4-5-6.md:12 |
| DR-03 | shared/api-docs/sapp.md dòng 2787-2789 | "Every field and query value is … forwarded as given" | Query rỗng/null bị bỏ trước khi gửi Ops/LMS (chỉ body forward nguyên) | sapp-26297.common.service.ts:96-103,115,163,215 |
| DR-04 | shared/api-docs/sapp.md §Flows 5–6 (dòng 2839) | POST /hubspot/contacts "Creates the contact, or adopts the one that already holds that email" | Khi dùng lại còn ghi đè property của contact cũ bằng property gửi lên — doc không nói | hubspot/common.service.ts:617-621,654-660 |
| DR-05 | be/.agents/maps/modules/tool.md:34 | Mô tả tool "Tool test chuyển lớp học viên" | Tool là chuyển nhượng học viên (transfer seat), không phải chuyển lớp | sapp-26297-tool.tsx:211 · 1786300000000-seed-tool-sapp-26297.ts:18 |
| DR-06 | shared/api-docs/sapp.md (dòng 2787-2789) vs màn | Ngầm hiểu chuỗi rỗng là một cách "gửi sai" | FE không bao giờ gửi chuỗi rỗng (bỏ field); với HubSpot, DTO nói rỗng = xóa property nhưng FE không gửi được rỗng (xem O-01) | sapp-26297-step-call.tsx:110-116 · sapp-26297.dto.ts:112 |
8. Câu hỏi cho BA (code không trả lời được) ​
| ID | Câu hỏi | Vì sao cần | Chặn bước sau? |
|---|---|---|---|
| Q-01 | Ai là người dùng tool (QA SAPP, dev MIND, BA)? Code không có role: route @Public(), màn chỉ qua ToolGate | Viết actor/US Statement cho FS | Có |
| Q-02 | O-01: ô trống không xóa được property HubSpot ⇒ case 5.1 / 5.7 (và 6.3 khi contact bị dùng lại) không dựng được như mô tả. Cố ý hay bug? | Quyết định ghi BR hay OQ; ảnh hưởng luồng 5 khi guard được mở | Có |
| Q-03 | O-05: route GHI Ops/HubSpot dev/staging @Public() — cố ý (chấp nhận rủi ro vì chỉ dev/staging) hay tạm thời? | Mục bảo mật/quyền của FS | Có |
| Q-04 | O-02: case 3.1/3.2 không khóa nút khi transfer_to_class_id trống — cố ý hay bug? | Ghi rule validation | Không |
| Q-05 | O-03 / O-09: fallback user_y = user_x; dọn dữ liệu xóa cứng lịch sử và ép NORMAL — cố ý? | Ghi rule + cảnh báo dữ liệu | Không |
| Q-06 | O-07 / O-08: hsGetContact không dùng; pipelines/owners không tự chọn — dự định bổ sung hay bỏ? | Phạm vi FS | Không |
| Q-07 | Case 1.A: Kafka HubSpot chỉ bắn cho A, không cho B — BA SAPP muốn thế nào? (tool chỉ ghi chú) | Thuộc nghiệp vụ SAPP; FS tool chỉ ghi "đang chờ" | Không |
| Q-08 | Case 4.8: WITHDRAWN vẫn hiện trên LMS — đúng ý nghiệp vụ SAPP-28419? | Như trên | Không |
| Q-09 | Luồng 4, 5, 6 phụ thuộc 11 câu đang chờ SAPP (URL LMS, token học viên, shape /courses, pipeline/stage, guard, cột user_hubspot_enrollments, bảng log mail…). FS as-built có mô tả luồng 4–6 như "đã triển khai nhưng chưa chạy được" không? | Phạm vi và mức độ khẳng định của FS | Có |
| Q-10 | Migration seed sapp.011 đã chạy ở môi trường nào? (0045 ngày 2026-09-25 ghi "chưa chạy ở môi trường nào"; thiếu row thì màn bị khóa) | Xác nhận màn đang dùng được | Có |
| Q-11 | Các route GHI (transfer, revert, remove, HubSpot write) đã được chạy thật lần nào chưa? (0045/0046: chưa, tính đến 2026-09-25) | Mức "đã kiểm chứng" trong FS | Không |
| Q-12 | Mã lỗi thật của case 1.10 và 3.20 (hiện tool chỉ chấm success=false) | Hoàn thiện bảng mã | Không |
| Q-13 | "Guide QA của ticket SAPP-26297" (nguồn của mọi case/kỳ vọng) nằm ở đâu? Không có trong repo | Truy vết nguồn cho FS | Không |
| Q-14 | Staging chạy nhánh Ops nào? Màn ghi "Test trên nhánh sapp-sprint-49" cho cả hai môi trường | Mô tả môi trường trong FS | Không |
| Q-15 | O-06: nhãn "Ops" cho thao tác HubSpot/LMS — chấp nhận hay sửa? | UX copy | Không |
9. Nhật ký nguồn đã đọc ​
| File / section | Mục đích |
|---|---|
ba/_workflow/templates/feature-inventory/Template.md | Template |
ba/system-profile.md §3–§6 | Công thức truy vết, ranh giới ghi |
ba/projects/2026-SAPP-Works-tools/project-context.md (grep) | Phân nhóm, tên file đầu ra |
fe/app/works/sapp/tools/sapp-26297/page.tsx · fe/app/works/sapp/tools/layout.tsx | Route màn |
fe/src/ui/pages/works/sapp/tools/registry.ts · shell.tsx:418-439 | Sidebar, ToolGate |
fe/src/data/constants/tool-codes.ts:42 | Tool code sapp.011 |
fe/src/ui/components/works/sapp/sapp-26297-tool.tsx (toàn bộ) | Màn, fixture, env, tab |
fe/src/ui/components/works/sapp/sapp-26297-flows.ts (toàn bộ 1975 dòng) | Mọi luồng, case, bước, check |
fe/src/ui/components/works/sapp/sapp-26297-step-call.tsx (toàn bộ) | Gọi API, preview, xác nhận GHI, tự chấm |
fe/src/ui/components/works/sapp/sapp-26297-sql.ts (toàn bộ) | Bản sao SQL FE |
fe/src/ui/components/works/sapp/sapp-26878-step-ops.tsx:223-324 (grep) | fillTemplate, {today} (→ nền tảng) |
fe/src/ui/components/works/sapp/sapp-26878-step-sql.tsx (grep) | Xác nhận câu GHI (→ nền tảng) |
fe/src/ui/components/works/sapp/secret-gate.tsx (grep) | Xác nhận 26297 không dùng SecretGate |
fe/src/data/services/sapp/sapp-26297.service.ts (toàn bộ) | Hàm gọi API FE |
fe/src/data/constants/api.ts:339-376 | URL route |
be/apps/api/src/sapp/sapp-26297.controller.ts (toàn bộ) | 28 route, @Public, passthrough |
be/apps/api/src/sapp/sapp.module.ts · be/libs/modules/src/lib/sapp/module.ts (grep) | Đăng ký controller/service |
be/apps/api/src/sapp/sapp-ops.controller.ts:2404-2442 (grep) | Route SQL runner |
be/libs/modules/src/lib/sapp/sapp-26297.common.service.ts (toàn bộ) | Logic gọi Ops/LMS/HubSpot |
be/libs/interface/src/lib/dto/sapp/sapp-26297.dto.ts (toàn bộ) | DTO |
be/libs/constants/src/lib/sapp/sapp-26297.constants.ts (toàn bộ) | Khóa URL LMS, property HubSpot |
be/libs/constants/src/lib/sapp/sapp-26297-sql.constants.ts (toàn bộ) | Catalogue SQL BE |
be/libs/constants/src/lib/sapp/sapp-ops-sql.constants.ts (toàn bộ) | Gộp catalogue, check trùng id |
be/libs/constants/src/lib/sapp/sapp-ops-api.constants.ts (grep) | Path Ops |
be/libs/constants/src/lib/error-codes/error-codes.constants.ts:750-845 (grep) | 13042, 13058, 13059 |
be/libs/constants/src/lib/env-system/env-system.constants.ts:39-40 | Khóa SAPP_LMS_API_URL_* |
be/libs/constants/src/lib/hubspot/hubspot.constants.ts:57 | Association type 16 |
be/libs/modules/src/lib/hubspot/common.service.ts:560-669,915-1004 | createTicket, createOrReuseContact, get/updateObject, pipelines, owners |
be/libs/database/migrations/1786300000000-seed-tool-sapp-26297.ts | Seed sapp.011 |
be/.agents/maps/modules/tool.md (grep) | Đối chiếu map module |
shared/api-docs/sapp.md:2674-2871 | Contract SQL runner + tool 26297 |
shared/integration/README.md dòng #45BE, #46BE | Ledger |
shared/integration/0045-be-sapp-26297-transfer-tool.md | Handover luồng 1–3 |
shared/integration/0046-be-sapp-26297-flows-4-5-6.md | Handover luồng 4–6 |
shared/external/sapp/sapp-26297/README.md · 2026-09-25-mind-sapp-26297-cau-hoi.md | Sổ cái + 11 câu hỏi gửi SAPP |
Script so sánh (Python, chỉ đọc) FE sapp-26297-sql.ts ↔ BE sapp-26297-sql.constants.ts | Xác nhận 39 entry giống hệt; 20 đọc / 19 ghi |