# Module `sh-exchange` — Trao đổi báo cáo sát hạch CSĐT ↔ PC08 (hub)

> 13/7/2026 · Giai đoạn: **MVP · Painkiller** · Bản HTML: [sh-exchange.html](sh-exchange.html)
> Nguồn nghiệp vụ: [luồng CSĐT ↔ PC08 bản chuẩn](../../BC/FILE%20BC%20OTO/_md/00-luong-trao-doi-csdt-pc08.md) ·
> [cấu trúc XML](../../_ingested/06-cau-truc-bao-cao-xml.md) · [hiện trạng báo cáo Vĩnh An](../giai-doan-1-mvp/hien-trang-bao-cao-vinhan-2026-07-11.md)
> Trạng thái code: **chưa có — scaffold mới** (store module `opengate-app-sh-exchange`).

## 1. Vai trò

Số hoá **vòng khứ hồi thủ công** giữa cơ sở đào tạo (CSĐT) và **PC08** trong khâu sát hạch:
BC1 · BC2 (+công văn, DS hợp lệ, biên bản) đi — KQBC2 · SBD · tình trạng · SAT_HACH về.
Hiện Vĩnh An làm hoàn toàn tay: kết xuất XML từ FPT, gửi file, nhận file, **đối soát 582 hồ sơ bằng mắt**,
lưu rải rác thư mục theo ngày. Module giải đúng 4 cái đau: **kho file có cấu trúc · trạng thái vòng ·
đối soát tự động · lưu vết thanh tra**.

**P1 KHÔNG sinh XML** — FPT vẫn là nguồn; module quản lý + parse + đối soát. Sinh XML là P3
(thuộc module `report` phía WS trung tâm).

## 2. Vị trí kiến trúc — WS hub riêng, center-aware

```
WS "trao-doi-sat-hach"  (hub — KHÔNG thuộc WS đào tạo của trung tâm nào)
├─ center: 40023 Vĩnh An · 400xx TT khác…      ← mọi BO gắn center_id
├─ batch / file / entry  (vòng khứ hồi)
├─ Thành viên: NV các CSĐT (1 account join nhiều WS — CP membership)
│              → data scope: CHỈ thấy center của mình
│              (tương lai) cán bộ PC08 → thấy mọi center, trả file lên hub
└─ P3: WS trung tâm (module report) sinh XML → push sang hub qua API (federation CP)
```

Lý do hub (đã chốt 13/7): PC08 là đầu mối **N trung tâm ↔ 1 PC08** — đặt vào WS riêng của
Vĩnh An là đóng đinh mô hình 1‑1. Vĩnh An là **center đầu tiên**; module chạy đủ giá trị
ngay cả khi chỉ có 1 center và PC08 chưa tham gia (khi đó CSĐT upload cả 2 chiều).

**Phân biệt 2 module dễ nhầm:**

| | `sh-exchange` (doc này) | [`report`](report-2026-07-07.md) | [`sh-gateway`](sh-gateway-2026-07-07.md) |
|---|---|---|---|
| Phía | **CSĐT** ↔ PC08 (file XML/xlsx đợt sát hạch) | CSĐT → Sở XD (sinh báo cáo theo mẫu) | **TT sát hạch** → Cục CSGT (API QCVN 40, realtime) |
| WS | hub trao đổi | WS trung tâm | WS sát hạch (v3) |
| Nguồn dữ liệu | file upload (P1) / API từ WS trung tâm (P3) | student/course/dat cùng DB | thiết bị chấm thi |

## 3. Feature / BO / Action

| Feature | BO chính | Action |
|---|---|---|
| `center` | Trung tâm đào tạo (CSĐT) tham gia hub | view·create·edit |
| `batch` | **Đợt sát hạch** — 1 vòng khứ hồi với PC08 | view·create·edit·**close**·delete |
| `file` | **File trao đổi** (immutable, 2 chiều) | view·**upload**·download·delete |
| `entry` | **Hồ sơ thí sinh trong đợt** (parse từ XML) | view·export |

Đối soát là **màn hình của `batch`** (đọc `entry`), không cần feature riêng.

## 4. Dữ liệu chính (mọi bảng có `center_id`)

### Trung tâm (`center`) — *bảng `shx_center`*
| Trường | Kiểu | Ghi chú |
|---|---|---|
| Mã CSĐT | text | `MA_CSDT` — vd `40023` (Vĩnh An); khớp mã trong XML |
| Tên / mã cơ quan tỉnh | text | `TEN_CSDT` · `MA_SO_GTVT=40` (legacy) |
| Trạng thái | bool | |

### Đợt sát hạch (`batch`) — *bảng `shx_batch`*
| Trường | Kiểu | Ghi chú |
|---|---|---|
| center_id | ref | |
| Mã kỳ SH | text | `MA_KY_SH` (vd `4051962037`) — điền khi KQBC2/SAT_HACH về |
| Ngày thi dự kiến / thực tế | date | nguồn cảnh báo mốc (P2) |
| Các khóa trong đợt | 1‑n | `shx_batch_khoa`: mã khóa (vd `40023K26B021`), hạng, số HS |
| Trạng thái vòng | enum | xem §5 |
| Ghi chú / số công văn | text | vd 694/TTĐT‑VA, 706/BC |

### File trao đổi (`file`) — *bảng `shx_file`*
| Trường | Kiểu | Ghi chú |
|---|---|---|
| batch_id · center_id | ref | |
| Loại | enum | `BC1` · `BC2` · `CONG_VAN` · `DS_HOP_LE` · `BIEN_BAN` · `KQBC2` · `SBD` · `TINH_TRANG` · `SAT_HACH` |
| Hướng | enum | `di` (CSĐT→PC08) · `ve` (PC08→CSĐT) |
| File gốc | file | **immutable — không ghi đè**; xml/zip/xlsx/docx/pdf |
| Kết quả parse | jsonb | HEADER + thống kê (số bản ghi, khóa, hạng) — **đã strip ảnh base64** |
| Trạng thái parse | enum | `chua_parse` · `ok` · `loi` (+message) |
| Người upload / thời điểm | ref+ts | lưu vết |

### Hồ sơ trong đợt (`entry`) — *bảng `shx_entry`*
| Trường | Kiểu | Ghi chú |
|---|---|---|
| batch_id · center_id | ref | |
| **MA_DK + SO_HO_SO** | text | **khóa khớp** xuyên suốt các thông điệp |
| Họ tên · ngày sinh · CMT/CCCD | text | từ BC1/BC2 |
| Khóa · hạng | text | |
| Trạng thái hồ sơ | enum | xem §5 |
| KQ duyệt | enum+text | `KQ_BC2` (1=hợp lệ) · `BC2_KQ_LYDO_TUCHOI` |
| SBD · lần SH | text | từ KQBC2/SBD |
| Kết quả thi | enum | `KET_QUA_SH`: **DA/RO** + điểm 4 phần `L/M/H/Đ` + `NX_SH_*` |
| Số GPLX · QĐ trúng tuyển | text | khi đạt |

## 5. Máy trạng thái

**Đợt** (BC1 gắn mức *khóa*, có thể upload bất kỳ lúc — không chặn vòng):
```
chuan_bi → da_gui (BC2+phụ lục) → da_duyet (KQBC2 về) → co_sbd → da_thi → co_ket_qua → dong
```
**Hồ sơ:**
```
de_nghi → hop_le | tu_choi(lý do) → co_sbd → dat | rot (+điểm, SO_GPLX)
```
Trạng thái đợt **suy ra từ file đã nhận** (upload KQBC2 → tự chuyển `da_duyet`), cho phép sửa tay khi lệch.

## 6. Quy tắc nghiệp vụ

1. **File đã upload không sửa/ghi đè** — sai thì upload bản mới, bản cũ giữ vết (phục vụ thanh tra).
2. **Tự phân loại khi upload**: theo **tên file + nội dung**, KHÔNG theo root tag —
   quirk FPT: file `BC2_*` có root `<BAO_CAO1>`. Luật: `BC1_*`→BC1 · `BC2_*`→BC2 ·
   `KQBC2_*`→KQBC2 · `40_*` root `<SAT_HACH>`→SAT_HACH · xlsx/docx→gợi ý theo tên, người dùng xác nhận.
3. **Parse → khớp hồ sơ qua `MA_DK`+`SO_HO_SO`**: BC1/BC2 tạo entry; KQBC2/SAT_HACH cập nhật entry
   có sẵn; **không khớp được → đưa vào danh sách lệch** (màn hình đối soát), không tự tạo.
4. **Strip ảnh `ANH_CHAN_DUNG` (base64 .jp2)** khỏi payload parse/preview — file gốc giữ nguyên.
5. Giữ nguyên **typo cố định của schema FPT** (`HANG_DE_NGHi_SH`) khi map — không "sửa cho đúng".
6. **Data scope theo center**: user center A không thấy dữ liệu center B (PII học viên).
   1 center thì role thường là đủ; **điều kiện tiên quyết khi lên ≥2 center** — xem
   [plan data-scope](../giai-doan-1-mvp/plan-data-scope-2026-07-06.md).
7. Mốc cứng **DAT −10 ngày · BC2 −7 ngày** trước ngày thi → cảnh báo (P2).
8. Mã đơn vị (`40023`, `40`) là **cấu hình theo center**, không hardcode.

## 7. Lộ trình

| Phase | Nội dung |
|---|---|
| **P1** | Đợt + upload → tự phân loại → parse → khớp entry · bảng đối soát (gửi/hợp lệ/từ chối+lý do/SBD/đạt‑rớt, highlight lệch) · dashboard đợt · kho file immutable · 1 center (Vĩnh An) |
| **P2** | Cảnh báo mốc DAT−10/BC2−7 · export đối soát + thống kê đạt‑rớt theo khóa/hạng · sổ đã nộp (registry) · data scope multi-center |
| **P3** | Nhận XML qua **API từ WS trung tâm** (module `report` sinh BC1/BC2 từ student/course rồi push) · vai trò PC08 trả file trên hub · ký số |

## 8. Liên kết chéo

| Đối tác | Quan hệ |
|---|---|
| `report` (WS trung tâm) | P3: sinh XML → push API sang hub; hub trả trạng thái duyệt/kết quả về |
| `sh-gateway` (WS sát hạch) | **khác tuyến** — tái dùng schema thông điệp nếu sau này làm phía TT sát hạch |
| `catalog` | enum hạng GPLX, bảng mã `KQ_BC2`/`KET_QUA_SH`/nội dung thi `L·M·H·Đ` |
| CP (Control Plane) | membership đa‑WS (NV trung tâm join hub bằng account sẵn có) |

## 8b. Onboard trung tâm ngoài Vĩnh An (multi-center — chốt 17/7)

Hub phục vụ **nhiều CSĐT**. Câu hỏi "trung tâm khác vào hệ thống bằng cách nào" → **không xây
web upload riêng**; cho họ **đăng nhập chính hub** để vừa upload vừa xem trạng thái/đối soát của
mình. PC08 tự onboard ngay trong màn **Quản lý trung tâm**, không cần admin nền tảng.

### Luồng mời
```
PC08 → Quản lý trung tâm → dòng center 400xx → "Mời trung tâm"
  → hub gọi CP tạo invitation { username: "400xx" (= mã CSDT), email (nếu có), TTL 7 ngày }
  → hiện link + copy → PC08 gửi link qua ZALO cho người đại diện trung tâm
  → trung tâm mở link: đặt tên hiển thị + mật khẩu → account username = 400xx
  → login sathach.sunstech.vn → tự nhận ROLE MẶC ĐỊNH của WS (= quyền trung tâm)
  → shexchange auto-bind center theo username == ma_csdt → chỉ thấy dữ liệu center mình
```

### Quyết định
| # | Quyết định | Lý do |
|---|---|---|
| O1 | **Đăng nhập chung hub** (không web riêng) | Trung tâm cần xem trạng thái duyệt/đối soát, không chỉ upload mù; hub sinh ra để nhiều CSĐT dùng chung — không nuôi 2 hệ auth |
| O2 | **Username = mã CSDT** (vd `40023`) — tài khoản **đơn vị** | Định danh ổn định, PC08 nhìn biết ngay, khớp `ma_csdt` trong XML. ⚠ Đánh đổi: audit ghi "40023 upload", không biết nhân viên nào — chấp nhận ở quy mô này; center đông người sau này mời thêm account cá nhân (schema `shexchange_center_user` nhiều‑nhiều đã sẵn) |
| O3 | **Role gán theo DEFAULT của WS** lúc join (không mang role trong lời mời) | Engine `ensureDefaultAssignment` sẵn có: chưa có role → nhận role `is_default` của WS. Trên hub, role mặc định (`workspace-member`) = **quyền trung tâm (7 quyền)**: `center.view` · `batch.view` · `file.view/upload/download` · `entry.view/export` — chỉ dữ liệu của mình. KHÔNG: tạo/sửa/đóng đợt (PC08 tạo đợt gán cho TT), sửa hồ sơ (đính chính = PC08), quản lý/xóa center, xóa file |
| O4 | **Scope theo center** (điều kiện tiên quyết khi ≥2 center — PII học viên) | Binding `user ↔ center`; user có binding → mọi query/upload tự lọc `center_id` của mình; PC08/owner (không binding) → thấy tất cả. Auto-bind lần đầu theo `username == ma_csdt`; bảng `shexchange_center_user` cho 1 center nhiều account về sau |
| O5 | **Link mời an toàn** | Bearer token: TTL ngắn (7 ngày), one-time (accepted = khoá), thu hồi/gửi lại ngay trong màn Quản lý trung tâm; gửi đúng Zalo người đại diện |

### Vòng đời mời (hiện trên màn Quản lý trung tâm)
`chưa mời → đã gửi link (còn hạn / hết hạn) → đã kích hoạt` + nút copy link · thu hồi · gửi lại.

### Phân biệt với P3
Onboard này = trung tâm **đăng nhập hub upload tay**. Trung tâm nào mua OpenGate làm **WS đào tạo
riêng** (như Vĩnh An) thì nâng lên **hệ thống tự push XML qua API** (P3, §7) — không đổi kiến trúc,
chỉ đổi cách file vào hub.

## 9. Câu hỏi mở

- **Kênh nhận file thực tế** của Nga/Chi hiện nay (cổng web tải về? email? USB?) → quyết UX upload chiều về.
- **PC08 có sẵn sàng tham gia hub** (trả KQBC2/SBD/SAT_HACH trực tiếp)? — giá trị bùng nổ khi có; chưa có vẫn chạy.
- **Ai vận hành WS hub** (SUNS?) — trách nhiệm pháp lý PII học viên nhiều trung tâm.
- BC1 thực tế gửi **đầu khóa** hay gửi gộp cùng đợt sát hạch như bộ 6/2026? → ảnh hưởng UI gắn BC1 vào khóa.

## 10. Ma trận phân quyền (→ manifest & seed role)

### 10.1 Khai báo `permissions[]` trong `manifest.json`

```json
[
  { "group": "exchange", "feature": "center", "label": "Trung tâm đào tạo",
    "actions": ["view", "create", "edit"] },
  { "group": "exchange", "feature": "batch",  "label": "Đợt sát hạch",
    "actions": ["view", "create", "edit", "close", "delete"] },
  { "group": "exchange", "feature": "file",   "label": "File trao đổi",
    "actions": ["view", "upload", "download", "delete"] },
  { "group": "exchange", "feature": "entry",  "label": "Hồ sơ trong đợt",
    "actions": ["view", "export"] }
]
```

> ⚠️ Action đặc thù (`upload`, `close`) cần có trong validator manifest (như `generate` của `report`).

### 10.2 Ma trận vai trò × action (2 vai trò thực tế — chốt 17/7)

Hub có **đúng 2 vai trò**: **PC08** (cơ quan quản lý — toàn quyền, thấy MỌI center) và
**Trung tâm** (role mặc định của WS — bị giới hạn, **chỉ thấy center của mình** qua scope).

| Feature · Action | **PC08** (cơ quan quản lý) | **Trung tâm** (mặc định) |
|---|:--:|:--:|
| `center.view` | ✓ mọi center | ✓ **chỉ của mình** |
| `center.create` · `edit` · **mời trung tâm** | ✓ | — |
| `batch.view` | ✓ tất cả | ✓ **đợt của mình** |
| `batch.create` | ✓ (tạo đợt gán cho TT) | — |
| `batch.edit` · `close` · `delete` | ✓ | — |
| `file.view` · `download` | ✓ | ✓ (của mình) |
| `file.upload` | ✓ cả 2 chiều | ✓ (up báo cáo của mình) |
| `file.delete` | ✓ | — |
| `entry.view` · `export` | ✓ | ✓ (của mình) |
| `entry.edit` (đính chính) | ✓ | — |

→ **PC08** = full module (15 quyền). **Trung tâm** = **7 quyền**: `center.view` · `batch.view` ·
`file.view/upload/download` · `entry.view/export`. Cách ly chéo center bằng **binding
`shexchange_center_user`** (auto-bind `username == ma_csdt` lần đầu); PC08 không binding → thấy tất cả.

> ⚠ Quyết định đã chốt: **PC08 tạo đợt** (trung tâm không `batch.create`); **không chặn hướng
> upload** ở giai đoạn này (trung tâm up được cả 2 chiều, chặn hướng theo loại file làm sau).
> Permission key đầy đủ: `sh-exchange.exchange.<feature>.<action>`.
