Skip to content

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 repo mind_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" (xem feature-inventory-platform-20260930.md).

FieldValue
Projectba/projects/2026-SAPP-Works-tools
ModeB – As-built
Ngày đọc code2026-09-30
Commite77c47a
Điểm vào BA cung cấpSub-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ớiFE · BE (api) · hệ ngoài: SAPP Ops API, SAPP Ops DB (qua SQL runner), SAPP LMS API, HubSpot (portal dev/staging)
StatusDraft

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.
  • 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 runner sapp-ops/sql/* (FlowStepSql của 26878, SappOpsSqlCommonService), fillTemplate / OpsBag củ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.

IDLoạiTên / đường dẫn người dùng thấyBằng chứng
E-01Mà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-02API (Ops, đọc)GET {env}/sapp-26297/classes/:classId/transfer-candidates → Ops GET /api/v1/classes/:id/transfer-candidatesbe/apps/api/src/sapp/sapp-26297.controller.ts:78,103 · be/libs/constants/src/lib/sapp/sapp-ops-api.constants.ts:53-54
E-03API (Ops, GHI)PUT {env}/sapp-26297/classes/:classId/students/transfer → Ops PUT /api/v1/classes/:id/students/transfersapp-26297.controller.ts:128,151 · sapp-ops-api.constants.ts:44-45
E-04API (Ops, đọc)GET {env}/sapp-26297/classes/:classId/students/:studentId/transfer-recordsapp-26297.controller.ts:174,200 · sapp-ops-api.constants.ts:59-60
E-05API (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-06API (Ops, đọc)GET {env}/sapp-26297/classes/:classId/studentssapp-26297.controller.ts:282,302 · sapp-ops-api.constants.ts:21
E-07API (Ops, GHI – xóa mềm)DELETE {env}/sapp-26297/classes/student (có body) → Ops DELETE /api/v1/classes/studentsapp-26297.controller.ts:324,342 · sapp-ops-api.constants.ts:71
E-08API (LMS, đọc)GET {env}/sapp-26297/lms/courses + header X-Lms-Token → LMS GET <SAPP_LMS_API_URL_*>/coursessapp-26297.controller.ts:360,379 · be/libs/constants/src/lib/sapp/sapp-26297.constants.ts:11-25
E-09API (HubSpot, GHI)POST {env}/sapp-26297/hubspot/contactssapp-26297.controller.ts:399,418
E-10API (HubSpot, đọc)GET {env}/sapp-26297/hubspot/contacts/:contactIdsapp-26297.controller.ts:437,451
E-11API (HubSpot, GHI)POST {env}/sapp-26297/hubspot/ticketssapp-26297.controller.ts:465,485
E-12API (HubSpot, GHI – kích hoạt đồng bộ)PATCH {env}/sapp-26297/hubspot/tickets/:ticketIdsapp-26297.controller.ts:505,526
E-13API (HubSpot, đọc)GET {env}/sapp-26297/hubspot/tickets/:ticketIdsapp-26297.controller.ts:547,561
E-14API (HubSpot, đọc)GET {env}/sapp-26297/hubspot/ticket-pipelinessapp-26297.controller.ts:575,589
E-15API (HubSpot, đọc)GET {env}/sapp-26297/hubspot/ownerssapp-26297.controller.ts:603,617
E-16API 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-17Seed dữ liệuMigration đăng ký tool sapp.011 trong bảng tools, nhóm sappbe/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 &ZeroWidthSpace;

"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 &ZeroWidthSpace;

IDAi (role)Làm gìĐiểm vàoKết quả quan sát đượcBằng chứng
A-01TesterMở màn toolE-01Mà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-02TesterChọn môi trường dev hoặc staging (mặc định dev)E-01Mọi bước sau gọi route của môi trường đó; không có lựa chọn productionsapp-26297-tool.tsx:179,222-234 · sapp-26297.service.ts:16
A-03TesterĐọc cảnh báo đỏ về thao tác GHIE-01Banner: 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 productionsapp-26297-tool.tsx:238-244
A-04TesterNhậ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ừ tokensapp-26297-flows.ts:36-191 · sapp-26297-tool.tsx:196-201,246-269
A-05TesterChuyển tab luồngE-01Hiệ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-06TesterMở một caseE-01Hiệ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ướcsapp-26297-tool.tsx:131-173
A-07TesterỞ 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-15Nú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ó undosapp-26297-step-call.tsx:422-441,486-547 · sapp-26297-flows.ts:212-219
A-08Hệ 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-09Hệ 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-10TesterỞ bước SQL: xem câu SQL, nhập param (nếu có), bấm chạyE-16Chạ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-11TesterỞ 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-12Hệ 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") &ZeroWidthSpace;

IDAiLàm gìĐiểm vàoKết quả quan sát đượcBằng chứng
A-20TesterLấ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-02Trả nguyên văn Ops; mảng ứng viên ở data.data, mỗi ứng viên có eligible_classes; phân trang ở data.metadatasapp-26297-flows.ts:606-647 · sapp-26297.common.service.ts:105-117
A-21TesterChuyể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-03GHI 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ănsapp-26297-flows.ts:358-391 · sapp-26297.controller.ts:134-138 · sapp-26297.common.service.ts:119-128
A-22TesterĐọc bản ghi chuyển nhượng hiệu lực của người cho (dữ liệu popup hủy)E-04Có 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: nullsapp-26297-flows.ts:421-434,879-896,945-953 · sapp-26297.controller.ts:181-185
A-23TesterHủy chuyển nhượng (class_id = L1, student_id = người CHO)E-05GHI: 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-24TesterĐọc danh sách học viên lớp (trang 1, 50 dòng) để xem cờ nút can_transfer / can_revert_transferE-06Hàng ở data.students, phân trang metasapp-26297-flows.ts:436-450 · sapp-26297.common.service.ts:148-165
A-25TesterXó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-07GHI: 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-26TesterLấ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-08Trả nguyên văn LMS; token đi qua header X-Lms-Token, không lưusapp-26297-flows.ts:1334-1357 · sapp-26297.service.ts:100-115 · sapp-26297.common.service.ts:188-230
A-27TesterTạo contact HubSpot (email, firstname="Test", lastname="SAPP-26297", lop_dang_ky)E-09Tạ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_idsapp-26297-flows.ts:1530-1551 · be/libs/modules/src/lib/hubspot/common.service.ts:622-661
A-28TesterTạ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-11Ticket tạo ở stage mở, gắn contact (association type 16); tool ghi ticket_idsapp-26297-flows.ts:1553-1593 · hubspot/common.service.ts:567-601 · be/libs/constants/src/lib/hubspot/hubspot.constants.ts:57
A-29TesterKéo ticket sang Close success (PATCH hs_pipeline_stage)E-12GHI; theo ghi chú: kích hoạt webhook → Kafka hubspot_enrollment → đồng bộ thật phía SAPPsapp-26297-flows.ts:1595-1613 · sapp-26297.controller.ts:508-511
A-30TesterSửa property ticket (mã lớp, kéo ra stage mở)E-12GHI HubSpotsapp-26297-flows.ts:1662-1673,1716-1741
A-31TesterĐọc ticket (property đồng bộ + contact gắn)E-13Chấm properties.trang_thai_dong_bo_lms = SYNC_SUCCESS / SYNC_FAILURE; trống = đồng bộ chưa xongsapp-26297-flows.ts:1615-1630 · sapp-26297.common.service.ts:270-278
A-32TesterLấy pipeline ticket + stage và danh sách owner để chép id vào fixtureE-14, E-15Không có check, không tự ghi fixture — tester chép taysapp-26297-flows.ts:1512-1528

3.3 Danh mục luồng và case (từ sapp-26297-flows.ts) &ZeroWidthSpace;

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 / CaseTênBướcKỳ vọng (tool chấm/hiển thị)Bằng chứng
Chuẩn bị · DọnChạy trước mọi luồng, chỉ DB dev/stagingsapp-26297-flows.ts:538-592
0.1Kiểm tiền điều kiệnS: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àysapp-26297-flows.ts:544-573
0.2Dọn dữ liệu sau testS:clean (3 câu GHI) · GA,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 3Bsapp-26297-flows.ts:574-590
1 · Chuyển nhượngAC1,2,4,5,6,8,9sapp-26297-flows.ts:594-865
1.AHappy pathC:candidates · G biến thể · C:transfer · S:f1v · C:classStudents ×3 (L1, L2, F1) · G KafkaB 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=falsesapp-26297-flows.ts:602-705
1.1Lớp nhận = L1G · C:transfer (transfer_to_class_id=L1)Thành côngsapp-26297-flows.ts:707-719
1.2A đã có tiến độ tại L1S:c12 (GHI 3/10) · C:transfer · S:int400|100332sapp-26297-flows.ts:721-732
1.3B đã có tiến độ tại L2S:c33 · C:transfer · S:int400|100332sapp-26297-flows.ts:734-745
1.4B đang RESERVED tại L2S:c34 · C:transfer · S:int400|100332sapp-26297-flows.ts:747-758
1.5A tại F1 đang RESERVEDS:c36 · C:transfer · S:int400|100333 + đủ 5 replacementssapp-26297-flows.ts:760-783
1.6A không có mặt tại F1G · C:transferThành côngsapp-26297-flows.ts:785-795
1.7 / 1.8 / 1.9Trỏ sang 3.5 / 3.1 / 3.2G100332 / 100331 / 100331sapp-26297-flows.ts:797-813
1.10transfer_from == transfer_toC:transfer (A→A)Chỉ check success=false — mã chưa chốtsapp-26297-flows.ts:815-826
1.10bThiếu transfer_to_class_idC:transfer (ô trống)HTTP Ops 422sapp-26297-flows.ts:828-840
1.11Chuyển nhượng lại sau hủyG → 2.BThành côngsapp-26297-flows.ts:842-846
AC9Kiểm lại lúc confirmG (thao tác UI Ops) · C:transfer400|100332sapp-26297-flows.ts:848-863
2 · Hủy chuyển nhượngAC10, AC12; tiền đề 1.A đã chạysapp-26297-flows.ts:867-1030
2.AHappy pathC:transferRecord · C:revertTransfer · S:f2v · C:classStudents ×2Record đủ field · hủy OK · A@L1 can_transfer=true, can_revert=false · B@L2 can_revert=falsesapp-26297-flows.ts:874-922
2.BChuyển nhượng lại (AC12)C:transfer · S:f1v · GThành công, vẫn 1 dòng lịch sửsapp-26297-flows.ts:924-933
2.1Hủy 2 lầnC:revert ×2Lần 2 400|100337sapp-26297-flows.ts:936-942
2.2GET record khi không còn hiệu lựcC:transferRecordHTTP 200 + data: nullsapp-26297-flows.ts:945-953
2.3GET record với student = BC:transferRecorddata: nullsapp-26297-flows.ts:956-965
2.4L2 đã bị xóaG · S:c24 (GHI xóa mềm lớp + khôi phục) · C:revertVẫn hủy thành công phía Asapp-26297-flows.ts:968-981
2.5B đã bị xóa khỏi L2G · S:c25 · C:revertVẫn hủy thành côngsapp-26297-flows.ts:984-993
2.6B đã học tại L2G · S:c26 · C:transferRecord · C:revert · S:f2vtransfer_to_progress có giá trị · hủy OK · tiến độ B giữsapp-26297-flows.ts:996-1011
2.7F1 của A bị bảo lưu sau chuyển nhượngG · S:c36 · C:revert · S:f2vHủ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ượngSau mỗi case chạy 3.7 (S:int)sapp-26297-flows.ts:1032-1161
3.1Lớp nhận khác mônS:c31 · C:transfer (tester điền lớp) · S:int400|100331 transfer_receiving_class_invalidsapp-26297-flows.ts:1039-1057
3.2Lớp nhận là RevisionS:c32 · C:transfer · S:int400|100331sapp-26297-flows.ts:1059-1074
3.3 / 3.4Người nhận có tiến độ / không NORMALS:c33 / S:c34 · C:transfer · S:int400|100332sapp-26297-flows.ts:1076-1100
3.5Ngườ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→BC→B lần 1 100332; sau hủy A→B thì C→B thành côngsapp-26297-flows.ts:1102-1121
3.6Lớp đi kèm sai trạng thái (AC6)S:c36 · C:transfer · G câu FS · S:int400|100333, đủ 5 replacements, replacements.status=RESERVED; biến thể Revision dựng taysapp-26297-flows.ts:1123-1159
3B · Chặn xóa học viênTest trên sapp-sprint-49; 8 trạng thái bị chặn; xóa MỀM thậtsapp-26297-flows.ts:1163-1271
3.8 / 3.9 / 3.12 / 3.13 / 3.14Xóa 1 học viên TRANSFERED_TO / BE_TRANSFERRED / RESERVED / RETAKING / REASSIGNEDS:grpb (dựng type) · C:removeStudents400|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.10Bulk: 1 chuyển nhượng + 1 NORMALS:grpb · C:removeStudents · G verify DBHTTP 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.11Bulk: tất cả bị chặnS:grpb · C:removeStudents400|490037sapp-26297-flows.ts:1216-1225
3.15Xóa 1 học viên NORMALS:grpb · C:removeStudents · G khôi phục200, removed có user_x, skipped rỗngsapp-26297-flows.ts:1231-1242
3.16Học viên đã ở lớp Lesson khácG · C:removeStudents200 + invalid_student không rỗng (định danh HỌ TÊN)sapp-26297-flows.ts:1245-1255
UICheckbox trên UI Ops bị disableGKiểm tay; API không trả can_deletesapp-26297-flows.ts:1257-1269
3C · Chặn hủy chuyển nhượngsapp-26297-flows.ts:1273-1330
3.17Hủy hai lầnG · C:revert ×2Lần 2 400|100337sapp-26297-flows.ts:1279-1287
3.18 / 3.19Hủy cho C (chưa chuyển nhượng) / cho B (người nhận)C:revert400|100337sapp-26297-flows.ts:1290-1311
3.20class_id không phải Lesson (F1)C:revert · GChỉ check success=false — mã CLASS_NOT_FOUND chưa chốt dạngsapp-26297-flows.ts:1314-1328
4 · LMSSAPP-28974; cần token HỌC VIÊN; route trả 13058 tới khi có key URLsapp-26297-flows.ts:1402-1508
4.ATrước chuyển nhượng — mốcG · C:lmsCourses (token A)Body chứa L1sapp-26297-flows.ts:1409-1419
4.BSau chuyển nhượng — người CHOG · C:lmsCourses (A) · G UIBody KHÔNG chứa L1 và F1sapp-26297-flows.ts:1421-1439
4.CSau chuyển nhượng — người NHẬNC:lmsCourses (B) · G UIBody chứa L2 (highlight)sapp-26297-flows.ts:1441-1453
4.DSau hủyG · C:lmsCourses A · C:lmsCourses B · S:f2vA thấy lại L1, B vẫn thấy L2sapp-26297-flows.ts:1455-1465
4.1–4.8Hồi quy theo type: RESERVED, RETOOK, MOVED_OUT → ẩn; CANCELED, REASSIGNED, RETAKING, MOVED_IN, WITHDRAWN → hiệnS: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.FFilter cũ vẫn chạyC:lmsCourses (+ name/type/status) · GKhô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ạiSAPP-28978; end-to-end BLOCKED bởi guard "Ticket đã được xử lý"sapp-26297-flows.ts:1680-1817
5.0Chuẩn bị HubSpotC:hsTicketPipelines · C:hsOwners · C:hsCreateContactCó pipeline/stage/owner trong fixture · contact có lop_dang_ky = MA_LOP_CUsapp-26297-flows.ts:1686-1696
A.1Đồng bộ lần đầu (Bảo lưu)C:hsCreateTicket · C:hsUpdateTicket (Close) · C:hsGetTicket · S:hs5Ticket SYNC_SUCCESS; 3 dòng enrollment (RESERVED / REASSIGNED ×2) — dòng DB kiểm bằng mắtsapp-26297-flows.ts:1698-1708
BXá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 · GHiện tại: DB không đổi + log "Ticket đã được xử lý"sapp-26297-flows.ts:1710-1751
5.1–5.8Hoà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:hs5Mọi case gắn highlight BLOCKED; kết quả DB/mail kiểm bằng mắtsapp-26297-flows.ts:1645-1678,1752-1815
6 · MailSAPP-28980; mail kiểm tay trong hộp thư devsapp-26297-flows.ts:1826-1962
6.1E01 — mã lớp không khớp contactC:hsCreateContact (lop_dang_ky = MA_LOP_MOI_1) · C:hsCreateTicket · C:hsUpdateTicket · C:hsGetTicket · G checklist mailTicket SYNC_FAILURE; mail kiểm taysapp-26297-flows.ts:1833-1850
6.2E01 khối mã lớp rỗngGKiểm taysapp-26297-flows.ts:1852-1863
6.3E04 — không tạo được tài khoảnG · C:hsCreateContact · C:hsCreateTicket · C:hsUpdateTicket · C:hsGetTicket · GSYNC_FAILUREsapp-26297-flows.ts:1865-1881
6.4E03 — 2 mã cùng lỗiG · C ×4 như trên · GSYNC_FAILURE; 2 lỗi gộp 1 mail (kiểm tay)sapp-26297-flows.ts:1883-1898
6.5 / 6.6 / 6.7E03 hoàn tác / E05 theo khu_vuc / E05 fallbackGBLOCKED (phụ thuộc luồng 5)sapp-26297-flows.ts:1900-1934
6.8Mail 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 templateGDanh sách placeholder theo templatesapp-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 &ZeroWidthSpace;

4.1 Fixture (trạng thái màn, trình duyệt) &ZeroWidthSpace;

Entity / bảngFieldTypeBắt buộcMặc định / enumBằng chứng
Fixture (localStorage sapp-26297:fixture)class_id_l1, class_id_l2, foundation_class_id, subject_id, user_a, user_b, user_cstringTheo bước dùng tớiLuồng 1–4 (không gắn flows)sapp-26297-flows.ts:36-63 · sapp-26297-tool.tsx:25
Fixtureclass_id, user_x, user_ystringTheo bướcChỉ luồng 3B; user_y trống ⇒ dùng user_xsapp-26297-flows.ts:64-81 · sapp-26297-tool.tsx:69
Fixturelms_token_a, lms_token_b, lms_tokenstring (password)Luồng 4secret — không lưu trình duyệtsapp-26297-flows.ts:84-104 · sapp-26297-tool.tsx:43-56
Fixturelms_user_id, lms_class_idstringLuồng 4 hồi quy—sapp-26297-flows.ts:105-116
Fixturehs_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_2stringLuồng 5–6khu_vuc: từ thứ 2 là ACCA/CFA/CMA/Cert/Dip/CGMA (gợi ý)sapp-26297-flows.ts:119-172
Fixturecontact_id, ticket_id, hs_user_idstringLuồng 5–6Tự điền khi chạy bướcsapp-26297-flows.ts:173-190
Token template dựng sẵn{today} (YYYY-MM-DD giờ VN) …string—→ nền tảng fillTemplatefe/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) &ZeroWidthSpace;

Entity / bảngFieldTypeBắt buộcMặc định / enumBằng chứng
Body transfertransfer_from, transfer_to, transfer_to_class_idstringKhông (Ops tự validate)—be/libs/interface/src/lib/dto/sapp/sapp-26297.dto.ts:16-32
Body removeuser[], classId, reasonstring[] / stringKhôngFE mặc định reason = "test SAPP-26297"sapp-26297.dto.ts:34-45 · sapp-26297-flows.ts:469
Query candidatestransfer_from, page_index, page_size, text, filter_class_id, sort_typestringKhôngsort_type: LATEST (mặc định phía Ops) / A_Z; FE mặc định page 1 / size 20sapp-26297.dto.ts:47-70 · sapp-26297-flows.ts:611-618
Query studentspage_index, page_sizestringKhôngFE: 1 / 50sapp-26297.dto.ts:72-78 · sapp-26297-flows.ts:446-447
Query LMS coursespage_index, page_size, name, type, statusstringKhôngFE: 1 / 50sapp-26297.dto.ts:93-110 · sapp-26297-flows.ts:1352-1353
Header LMSX-Lms-TokenstringCó (thiếu ⇒ 13059)Tiền tố Bearer bị bỏsapp-26297.controller.ts:369 · sapp-26297.common.service.ts:205-209
Body HubSpotproperties (tên property nội bộ → giá trị), contactId (ticket)object / stringKhông—sapp-26297.dto.ts:112-127
Response Ops/LMSupstreamStatus, 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 &ZeroWidthSpace;

Entity / bảngFieldTypeBắt buộcMặc định / enumBằng chứng
HubSpot ticketsubject, 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_FAILUREsapp-26297.constants.ts:27-43 · sapp-26297.common.service.ts:270-278
HubSpot contactemail, firstname, lastname, lop_dang_ky, l_p_b_o_l_u_h_c_l_istring—lop_dang_ky tách dấu chấm phẩysapp-26297.constants.ts:45-51

4.4 Bảng Ops DB mà catalogue SQL s26297.* đọc / ghi &ZeroWidthSpace;

Entity / bảngFieldTypeBắt buộcMặc định / enumBằng chứng
class_user_instancestype (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_TRANSFERREDbe/libs/constants/src/lib/sapp/sapp-26297-sql.constants.ts:13-14,172-177,218-222,231-236,260-269,304-315
class_user_instanceslearning_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_instancesdeleted_at (GHI xóa mềm / khôi phục)timestamp——sapp-26297-sql.constants.ts:129-139,223-228
classesdeleted_at (GHI xóa mềm / khôi phục L2)timestamp——sapp-26297-sql.constants.ts:119-127
class_user_change_history_instancessubject_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_classesnormal_class_id → foundation_class_id (đọc)———sapp-26297-sql.constants.ts:57-60,190-193
user_hubspot_enrollmentsclass_code_target, class_action_type, user_id, sync_status_hubspot, deleted_at (đọc)———sapp-26297-sql.constants.ts:272-279
Tổng catalogue39 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 &ZeroWidthSpace;

Entity / bảngFieldTypeBắt buộcMặc định / enumBằ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 NOTHING1786300000000-seed-tool-sapp-26297.ts:13-19
env_systemsSAPP_LMS_API_URL_DEV, SAPP_LMS_API_URL_STAGING (URL gồm prefix LMS; dấu / cuối bị cắt)stringCó cho luồng 4Thiếu ⇒ 13058be/libs/constants/src/lib/env-system/env-system.constants.ts:39-40 · sapp-26297.common.service.ts:180-186
env_systemsCredentials Ops (SAPP_OPS_*) — → nền tảng auto-login———sapp-26297.common.service.ts:62-73

5. Rule & validation tìm thấy &ZeroWidthSpace;

IDRule (ngôn ngữ nghiệp vụ)LoạiÁp dụng ởBằng chứngNghi là bug?
R-01Vớ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ệnA-20…A-26sapp-26297.controller.ts:31-41,65-76 · sapp-26297.common.service.ts:30-40,90-93Không
R-02Chỉ 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ệnA-20…A-26sapp-26297.common.service.ts:75-88,217-225 · be/libs/constants/src/lib/error-codes/error-codes.constants.ts:750-754Không
R-03Mind_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)validationA-20…A-25sapp-26297.dto.ts:8-14Không
R-04Giá trị query rỗng / null bị bỏ trước khi gửi (Ops thấy như "không có")validationA-20, A-24, A-26sapp-26297.common.service.ts:96-103Không
R-05Trê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/queryvalidationA-07sapp-26297-step-call.tsx:96-116 · sapp-26297-flows.ts:221-224⚠️ xem R-30
R-06Nút Chạy bị khóa khi còn ô chứa {token} chưa được fixture thayvalidationA-07sapp-26297-step-call.tsx:428-430,490-495,541-547Không
R-076 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ệnA-21, A-23, A-25, A-27…A-30sapp-26297-flows.ts:211-219 · sapp-26297-step-call.tsx:431-441,500-502Không
R-08Câ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ệnA-10sapp-26878-step-sql.tsx:203-213 · sapp-ops-sql.constants.ts:7-13Không
R-09Mọ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ảigiới hạnA-10sapp-26297-flows.ts:321-349Không
R-10Catalogue 26878 và 26297 dùng chung không gian id; trùng id ⇒ API không khởi độnggiới hạnE-16sapp-ops-sql.constants.ts:10-27Không
R-11Luồ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ỏvalidationA-26sapp-26297.common.service.ts:195-209 · error-codes.constants.ts:834-845Không
R-12Token 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ệnA-26sapp-26297.service.ts:100-115 · sapp-26297.common.service.ts:188-194Không
R-13Token đă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 passwordgiới hạnA-04sapp-26297-tool.tsx:39-56 · sapp-26297-flows.ts:29-33Không
R-14Fixture chỉ đưa giá trị không rỗng (đã trim) vào bước; user_y trống ⇒ dùng user_xcông thứcA-04, A-25sapp-26297-tool.tsx:58-71⚠️ xem O-03
R-15Thao 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ệnA-27…A-32sapp-26297.controller.ts:51-63 · sapp-26297.service.ts:117-118Không
R-16Tạ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ệnA-27hubspot/common.service.ts:607-661Không
R-17Tạ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ệnA-28sapp-26297.common.service.ts:244-254 · hubspot/common.service.ts:567-601 · hubspot.constants.ts:57Không
R-18lop_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-28sapp-26297.constants.ts:30-31 · sapp-26297.common.service.ts:234-239 · sapp-26297-flows.ts:1545,1581,1586Khô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áiA-29sapp-26297.common.service.ts:256 · sapp-26297-flows.ts:1600,1515Khô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ứcA-08sapp-26297-flows.ts:393-397Không
R-21"Cập nhật thành công" = HTTP 200, success=true, data.message="Update successfully"công thứcA-08sapp-26297-flows.ts:399-403Không
R-22Check replacements-keys: đủ khi mỗi khóa có giá trị khác rỗng/nullcông thức1.5, 3.6sapp-26297-step-call.tsx:281-293Không
R-23Check "ứ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 = 0công thức1.Asapp-26297-step-call.tsx:351-370Không
R-24Check 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ạn1.A, 2.Asapp-26297-step-call.tsx:381-393 · sapp-26297-flows.ts:446-447Không
R-25Mộ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ức3.10, 3.15sapp-26297-step-call.tsx:217-224 · shared/integration/0045-be-sapp-26297-transfer-tool.md:52-54Không
R-26Check "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ớpcông thức4.A–4.D, 4.1–4.8sapp-26297-step-call.tsx:233-241,371-380 · sapp-26297-flows.ts:1359-1367,1413-1416⚠️ xem O-04
R-27Tool 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–3sapp-26297-flows.ts:723-728,1041-1051,1102-1115,1125-1150,937-941,1172-1178,828-839Không
R-288 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 3Bsapp-26297-flows.ts:1185Không
R-29LMS ẩ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 4sapp-26297-flows.ts:1404-1406,1466-1483 · sapp-26297.constants.ts:19-24Khô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"validation5.1, 5.7, 6.3sapp-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-31Khô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ạnToàn toolsapp-26297.controller.ts:39-40 · sapp-26297.constants.ts:8-16 · sapp-26297.service.ts:16 · sapp-26297-tool.tsx:223 · api.ts:339Không
R-32Chuỗ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 Opsvalidation (dữ liệu)A-10sapp-26297-flows.ts:10-11 · sapp-26297-sql.constants.ts:13-14Không
R-33Chỉ khi có đủ row sapp.011 thì màn mở (ToolGate fail-closed → nền tảng)điều kiệnA-011786300000000-seed-tool-sapp-26297.ts:6-8Khô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_FAILURE

Bằ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 &ZeroWidthSpace;

LoạiChi tiếtBằng chứng
Quyền (API)Cả 28 route @Public() — không cần JWT; kể cả route GHI Ops/HubSpotsapp-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 SecretGateshell.tsx:427-435 · tool-codes.ts:42 · (không có tham chiếu SecretGate trong sapp-26297-tool.tsx)
Rào chắn prodKhông route prod ở controller / FE / khóa env_systems; banner cảnh báo trên mànsapp-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 API6 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 APIGET /courses bằng token học viên; base URL từ env_systemssapp-26297.common.service.ts:178-230
Tích hợp ngoài — HubSpotTạ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 DBQua SQL runner dùng chung, id s26297.*sapp-ops-sql.constants.ts:29-32 · sapp-26297-tool.tsx:96-107
Thông báomind_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 taysapp-26297-flows.ts:1819-1824,1826-1831
Job / cronKhô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)
LogLỗ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) &ZeroWidthSpace;

IDQuan sátBằng chứngCâu hỏi
O-01Case 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ònsapp-26297-step-call.tsx:110-116 · sapp-26297-flows.ts:1661-1672,1757-1758,1808,1872-1873 · hubspot/common.service.ts:947-950Q-02
O-02Case 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ì 100331sapp-26297-flows.ts:1049-1053,1066-1070 · sapp-26297-step-call.tsx:428-430Q-04
O-03user_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ỗisapp-26297-tool.tsx:69 · sapp-26297-flows.ts:1197-1207Q-05
O-04Check "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 3sapp-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-60Q-09
O-05Toà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 SAPPsapp-26297.controller.ts:128-172,228-280,324-358,399-545 · shared/api-docs/sapp.md:2760Q-03
O-06Hộ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ốngsapp-26297-step-call.tsx:437,551Q-15
O-07Thao tác hsGetContact và route GET …/hubspot/contacts/:contactId tồn tại nhưng không bước nào dùngsapp-26297-flows.ts:204 · sapp-26297-step-call.tsx:73-76,164-165 · sapp-26297.controller.ts:437-463Q-06
O-08Bướ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 taysapp-26297-flows.ts:1512-1528Q-06
O-09Dọ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ốcsapp-26297-sql.constants.ts:231-249Q-05

7. Drift — tài liệu nói khác code &ZeroWidthSpace;

IDTài liệu (file + section)Tài liệu nóiCode đang làmBằng chứng
DR-01shared/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ậtsapp-26297-flows.ts:1964-1974 · sapp-26297.controller.ts:360-629
DR-02shared/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-03shared/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-04shared/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óihubspot/common.service.ts:617-621,654-660
DR-05be/.agents/maps/modules/tool.md:34Mô 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ớpsapp-26297-tool.tsx:211 · 1786300000000-seed-tool-sapp-26297.ts:18
DR-06shared/api-docs/sapp.md (dòng 2787-2789) vs mànNgầ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) &ZeroWidthSpace;

IDCâu hỏiVì sao cầnChặn bước sau?
Q-01Ai là người dùng tool (QA SAPP, dev MIND, BA)? Code không có role: route @Public(), màn chỉ qua ToolGateViết actor/US Statement cho FSCó
Q-02O-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-03O-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 FSCó
Q-04O-02: case 3.1/3.2 không khóa nút khi transfer_to_class_id trống — cố ý hay bug?Ghi rule validationKhông
Q-05O-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ệuKhông
Q-06O-07 / O-08: hsGetContact không dùng; pipelines/owners không tự chọn — dự định bổ sung hay bỏ?Phạm vi FSKhông
Q-07Case 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-08Case 4.8: WITHDRAWN vẫn hiện trên LMS — đúng ý nghiệp vụ SAPP-28419?Như trênKhông
Q-09Luồ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 FSCó
Q-10Migration 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 đượcCó
Q-11Cá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 FSKhông
Q-12Mã 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 repoTruy vết nguồn cho FSKhông
Q-14Staging chạy nhánh Ops nào? Màn ghi "Test trên nhánh sapp-sprint-49" cho cả hai môi trườngMô tả môi trường trong FSKhông
Q-15O-06: nhãn "Ops" cho thao tác HubSpot/LMS — chấp nhận hay sửa?UX copyKhông

9. Nhật ký nguồn đã đọc &ZeroWidthSpace;

File / sectionMục đích
ba/_workflow/templates/feature-inventory/Template.mdTemplate
ba/system-profile.md §3–§6Cô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.tsxRoute màn
fe/src/ui/pages/works/sapp/tools/registry.ts · shell.tsx:418-439Sidebar, ToolGate
fe/src/data/constants/tool-codes.ts:42Tool 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-376URL 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-40Khóa SAPP_LMS_API_URL_*
be/libs/constants/src/lib/hubspot/hubspot.constants.ts:57Association type 16
be/libs/modules/src/lib/hubspot/common.service.ts:560-669,915-1004createTicket, createOrReuseContact, get/updateObject, pipelines, owners
be/libs/database/migrations/1786300000000-seed-tool-sapp-26297.tsSeed sapp.011
be/.agents/maps/modules/tool.md (grep)Đối chiếu map module
shared/api-docs/sapp.md:2674-2871Contract SQL runner + tool 26297
shared/integration/README.md dòng #45BE, #46BELedger
shared/integration/0045-be-sapp-26297-transfer-tool.mdHandover luồng 1–3
shared/integration/0046-be-sapp-26297-flows-4-5-6.mdHandover luồng 4–6
shared/external/sapp/sapp-26297/README.md · 2026-09-25-mind-sapp-26297-cau-hoi.mdSổ 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.tsXác nhận 39 entry giống hệt; 20 đọc / 19 ghi