# Module `portal` — Backend app học viên

> 07/7/2026 · Giai đoạn: **v1.5** · WS đào tạo · Bản HTML: [portal.html](portal.html) · Sơ đồ tổng thể: [../so-do-ws-module-feature-2026-07-06.md](../so-do-ws-module-feature-2026-07-06.md)
> Trạng thái: Chưa có trong code — scaffold ở v1.5. App mobile là client riêng; module này là API + dữ liệu phía WS.

## 1. Vai trò
Backend cho **app học viên** (iOS/Android) — công cụ tuyển sinh khác biệt: tra tiến độ km/giờ DAT thời gian thực, lịch học, **đặt lịch bổ túc**, ôn 600 câu, thông báo **Zalo OA**.

## 2. Feature / BO / Action

| Feature | BO chính | Action |
|---|---|---|
| `account` | Tài khoản học viên trên app | view·create·**reset** |
| `progress` | Tra cứu tiến độ km/giờ | view |
| `booking` | Đặt lịch bổ túc với GV | view·approve·**book** |
| `practice` | Ôn 600 câu, thi thử trên app | view |

## 3. Dữ liệu chính

| BO | Trường chính |
|---|---|
| **Tài khoản app** | Học viên (ref `ma_dk`/CCCD) · SĐT đăng nhập · thiết bị · trạng thái |
| **Booking** | Học viên × GV × khung giờ · loại (bổ túc sa hình/đường trường) · trạng thái (đặt → GV/giáo vụ duyệt → hoàn thành/hủy) |
| **Tiến độ (view)** | Đọc từ dat.reconcile: % km/giờ, còn thiếu, cảnh báo — **chỉ dữ liệu của chính mình** |

## 4. Quy tắc nghiệp vụ
1. Học viên **self-scope**: chỉ thấy hồ sơ/tiến độ/công nợ của mình
2. Booking phải được GV hoặc giáo vụ duyệt; lịch trùng bị chặn
3. Thông báo đẩy qua **notify** (Zalo OA/push) — portal không tự gửi
4. Mapping tài khoản ↔ học viên qua SĐT/CCCD (plan data-scope P3)

## 5. Liên kết chéo
- **student** — hồ sơ của tôi
- **dat** — tiến độ km/giờ
- **tuition** — công nợ của tôi
- **lms** — 600 câu, thi thử
- **notify** — Zalo OA/push
- **booking/chip (v2)** — đặt phiên xe chip

## 6. Câu hỏi mở
- Đặt lịch bổ túc tính phí ngay (nối tuition) hay miễn phí?
- App dùng chung nhiều trung tâm (multi-tenant client) — chọn trung tâm thế nào?

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

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

```json
[
  {
    "group": "main",
    "feature": "account",
    "label": "Tài khoản app",
    "actions": [
      "view",
      "create",
      "reset"
    ]
  },
  {
    "group": "main",
    "feature": "progress",
    "label": "Tiến độ km/giờ",
    "actions": [
      "view"
    ]
  },
  {
    "group": "main",
    "feature": "booking",
    "label": "Đặt lịch bổ túc",
    "actions": [
      "view",
      "approve",
      "book"
    ]
  },
  {
    "group": "main",
    "feature": "practice",
    "label": "Ôn tập & thi thử",
    "actions": [
      "view"
    ]
  }
]
```

> ⚠️ Action đặc thù (`book`, `reset`) cần bổ sung vào validator manifest — xem [plan data-scope P1](../giai-doan-1-mvp/plan-data-scope-2026-07-06.md).

### 7.2 Ma trận vai trò × feature (seed `rt_role`)

| Feature | `QTHT` | `GIAO_VU` | `GIAO_VIEN` | `HOC_VIEN` |
|---|:--:|:--:|:--:|:--:|
| `account` | W | W | — | — |
| `progress` | — | — | — | R² |
| `booking` | — | R·A | R·A¹ | W² |
| `practice` | — | — | — | R² |

> **R** xem · **W** tạo/sửa · **A** duyệt · **—** không có quyền · ¹ dữ liệu **phân công** · ² **của chính mình** · ³ **theo trạng thái** hồ sơ. Vai trò không liệt kê = không có quyền. Permission key đầy đủ: `portal.main.<feature>.<action>`.
> Đặc thù: account +reset · booking +book (HOC_VIEN) · hủy lịch = bước workflow

### 7.3 Quyền mức record (row-level)

**(a) Manifest — bổ sung `fields` + `scopeVars` vào các feature bị scope** *(mở rộng schema theo [plan data-scope P1](../giai-doan-1-mvp/plan-data-scope-2026-07-06.md) — manifest chỉ khai "cái gì scope được", KHÔNG chứa rule)*:

```json
[
  {
    "group": "main",
    "feature": "progress",
    "fields": [
      "nguoi_lxid"
    ],
    "scopeVars": [
      "current_nguoi_lxid"
    ]
  },
  {
    "group": "main",
    "feature": "booking",
    "fields": [
      "nguoi_lxid",
      "giao_vien_id"
    ],
    "scopeVars": [
      "current_nguoi_lxid",
      "current_giao_vien_id"
    ]
  },
  {
    "group": "main",
    "feature": "practice",
    "fields": [
      "nguoi_lxid"
    ],
    "scopeVars": [
      "current_nguoi_lxid"
    ]
  }
]
```

**(b) Điều kiện record theo vai trò:**

| Vai trò | Feature (action) | Điều kiện record |
|---|---|---|
| `HOC_VIEN` | progress, practice (view) · booking (view, book) | nguoi_lxid = ctx.current_nguoi_lxid |
| `GIAO_VIEN` | booking (view, approve) | giao_vien_id = ctx.current_giao_vien_id (lịch của mình) |

**(c) Seed `rt_role.grants` (kind `rule` — nhiều rule cùng resource+action = OR):**

```json
[
  {
    "kind": "rule",
    "module": "portal",
    "feature": "progress",
    "actions": [
      "view"
    ],
    "predicate": {
      "op": "cmp",
      "field": "nguoi_lxid",
      "cmp": "=",
      "value": {
        "ctx": "current_nguoi_lxid"
      }
    }
  },
  {
    "kind": "rule",
    "module": "portal",
    "feature": "booking",
    "actions": [
      "view",
      "book"
    ],
    "predicate": {
      "op": "cmp",
      "field": "nguoi_lxid",
      "cmp": "=",
      "value": {
        "ctx": "current_nguoi_lxid"
      }
    }
  },
  {
    "kind": "rule",
    "module": "portal",
    "feature": "booking",
    "actions": [
      "view",
      "approve"
    ],
    "predicate": {
      "op": "cmp",
      "field": "giao_vien_id",
      "cmp": "=",
      "value": {
        "ctx": "current_giao_vien_id"
      }
    }
  }
]
```

> Biến ngữ cảnh: `current_user_id` — engine có sẵn · `current_giao_vien_id`/`current_ma_gv` — module teacher tra từ user đăng nhập (ctxVars, plan P3) · `current_nguoi_lxid`/`current_ma_dk_list` — module student tra (1 người nhiều lượt đăng ký) · `current_assigned_*` — danh sách phụ trách/được phân công.
