# Hướng dẫn gắn AI quét giấy tờ – TravelQ ID (d22)

TravelQ ID có sẵn một "ổ cắm" để gắn **dịch vụ AI đọc giấy tờ** (CCCD, hộ chiếu) của bất kỳ nhà cung cấp nào, **chỉ bằng cấu hình** (biến môi trường), không phải sửa mã.

> Mặc định **TẮT**. Khi tắt, TravelQ ID không gửi ảnh đi đâu cả và `POST /api/v1/me/wizard/doc-read` trả `503 docai_unavailable`.

## 1. Khi nào AI được gọi?

AI chỉ là **phương án dự phòng, im lặng**. Nó được gọi khi đủ cả 4 điều kiện:

1. Máy chủ đã bật (`DOCAI_ENABLED=1` và có `DOCAI_URL` hợp lệ).
2. Khách đã **tick đồng ý** ở bước Xác nhận (quy tắc d21: trước khi đồng ý, ảnh chỉ nằm trên điện thoại của khách).
3. Điện thoại của khách **không tự đọc được** mặt trước CCCD (QR) hoặc trang hộ chiếu (MRZ).
4. Ảnh mặt trước / trang hộ chiếu đã được tải lên. Máy chủ so mã SHA-256 để chắc chắn AI đọc đúng ảnh đó.

Mỗi hồ sơ chỉ gọi **một lần**, giới hạn 10 lần/giờ/tài khoản. Chỉ gửi mặt trước CCCD hoặc trang ảnh hộ chiếu.

**Kết quả được dùng thế nào:**

- AI **chỉ điền ô khách chưa nhập**. Hiện tại đó là ngày sinh và giới tính, vì giao diện không có ô nhập cho hai trường này.
- AI **không bao giờ ghi đè** những gì khách đã gõ.
- Nếu tên hoặc năm sinh do AI đọc khác với khách nhập, hồ sơ được gắn cờ "lệch" để nhân viên xem lại. Cờ này chỉ ghi tên trường, không ghi giá trị.

**Khi AI lỗi:** lỗi, hết giờ hay sai khoá đều **im lặng**. Khách không thấy thông báo nào và vẫn được cấp ID như khi tự nhập.

## 2. Cấu hình (biến môi trường)

| Biến | Bắt buộc | Ý nghĩa |
|---|---|---|
| `DOCAI_ENABLED` | có | `1` / `true` / `yes` / `on` để bật. Không có thì tắt. |
| `DOCAI_URL` | có | Địa chỉ nhận yêu cầu, **phải là https**. Chỉ cho phép http với `localhost` / `127.0.0.1` để thử. |
| `DOCAI_API_KEY` | tuỳ | Gửi dưới dạng `Authorization: Bearer <khoá>`. **Không bao giờ** được in ra log, trả về API hay hiện ở /admin; hệ thống chỉ biết "có khoá". |
| `DOCAI_TIMEOUT_MS` | tuỳ | Thời gian chờ, mặc định `8000`, giới hạn 1000–30000. Quá giờ thì bỏ qua im lặng. |
| `DOCAI_PROVIDER_NAME` | tuỳ | Tên hiển thị trong nhật ký, ví dụ `my-ai`. Mặc định `http`. |
| `DOCAI_FORMAT` | tuỳ | `json` (mặc định, ảnh base64) hoặc `multipart` (tệp `image`). |
| `DOCAI_MIN_CONFIDENCE` | tuỳ | Ngưỡng độ tin cậy, mặc định `0.8`. Trường dưới ngưỡng hoặc không có độ tin cậy sẽ bị bỏ. |

**Ví dụ trên Netlify:** vào Site settings → Environment variables, thêm các biến sau rồi deploy lại:

```
DOCAI_ENABLED=1
DOCAI_URL=https://ai.nha-cung-cap.example/v1/read
DOCAI_API_KEY=<khoá do nhà cung cấp cấp – không dán vào mã, không gửi qua chat>
DOCAI_TIMEOUT_MS=8000
DOCAI_PROVIDER_NAME=my-ai
```

Muốn tắt thì xoá `DOCAI_ENABLED` hoặc đặt `DOCAI_ENABLED=0`.

## 3. Hợp đồng với nhà cung cấp

Bản đầy đủ nằm trong `openapi.yaml`, ở các mục `DocAiProviderRequest`, `DocAiProviderResponse` và `DocAiProviderError`.

### Yêu cầu TravelQ ID gửi đi

`POST <DOCAI_URL>` kèm các header:

- `content-type: application/json`
- `authorization: Bearer <DOCAI_API_KEY>`
- `x-request-id: <requestId>`
- `user-agent: TravelQ-ID-DocAI/1`

Thân yêu cầu:

```json
{
  "requestId": "3f1c2a9e-0b7d-4c55-9a51-2b8f4f0e6d11",
  "docType": "cccd_front",
  "lang": "vi",
  "images": [{ "side": "front", "mime": "image/jpeg", "data": "<base64>" }]
}
```

- `docType` nhận một trong các giá trị `cccd_front`, `cccd_back`, `passport`.
- Với `DOCAI_FORMAT=multipart`, yêu cầu gồm các trường `requestId`, `docType`, `lang` và tệp `image` (image/jpeg).
- TravelQ ID không theo chuyển hướng (redirect) và bỏ mọi phản hồi lớn hơn 64 KB.

### Phản hồi mong đợi (HTTP 200)

```json
{
  "ok": true,
  "fields": {
    "fullName": "Nguyễn Văn Mẫu", "dob": "1990-01-01", "sex": "M",
    "idNumber": "000000000202", "address": "1 Đường Mẫu, Phường Mẫu",
    "nationality": "VNM", "passportNumber": null, "expiry": null
  },
  "confidence": { "fullName": 0.97, "dob": 0.95, "sex": 0.99, "idNumber": 0.96, "address": 0.85, "nationality": 0.99 }
}
```

Định dạng từng trường:

- `dob` và `expiry`: `YYYY-MM-DD`.
- `sex`: `M`, `F` hoặc `X`.
- `nationality`: mã ISO 3 hoặc 2 chữ.
- `idNumber`: 12 chữ số.
- `passportNumber`: 6–9 chữ hoặc số.
- `confidence`: số từ 0 đến 1 cho **từng trường**.

### Lỗi

Nhà cung cấp trả HTTP 4xx/5xx kèm thân:

```json
{ "ok": false, "error": { "code": "unreadable", "message": "…" } }
```

Mã lỗi nên dùng:

- `unauthorized` (401)
- `unreadable` (422)
- `unsupported_document` (422)
- `too_large` (413)
- `rate_limited` (429)
- `internal` (500)

TravelQ ID tự ghi thêm các mã sau:

- `timeout`
- `network`
- `bad_response` (không phải JSON, hoặc quá 64 KB)
- `no_fields` (không trường nào đạt ngưỡng)
- `http_<mã>`

**Mọi lỗi đều được bỏ qua im lặng.** Khách tiếp tục tự nhập.

## 4. Thử với máy chủ giả lập (không cần nhà cung cấp thật)

```
node tools/docai-mock-server.mjs 8797           # http://127.0.0.1:8797/v1/read – dữ liệu MẪU
DOCAI_ENABLED=1 DOCAI_URL=http://127.0.0.1:8797/v1/read npm start
```

- Thêm `?mode=slow|error|unauthorized|badjson|lowconf|unreadable` vào `DOCAI_URL` để thử các tình huống lỗi.
- `GET /_last` cho xem yêu cầu gần nhất. Endpoint này chỉ trả siêu dữ liệu, không trả ảnh.
- Kiểm thử tự động gồm:
  - `node --test test/docai.test.mjs`;
  - `DOCAI=off|on|fail python3 test/d22_docai.test.py`.

## 5. Bảo mật và nhật ký

- Không gửi ảnh trước khi khách đồng ý. Không gửi khi điện thoại đã tự đọc được ảnh.
- Mỗi lượt gọi được ghi vào nhật ký `kyc.docai_read`, gồm: nhà cung cấp, loại giấy tờ, ok/lỗi, thời gian, trường đã điền, trường lệch và requestId.
- Nhật ký **không** chứa ảnh, giá trị đọc được hay khoá.
- Khi chọn nhà cung cấp, cần có hợp đồng xử lý dữ liệu cá nhân. Nhà cung cấp phải cam kết không lưu ảnh và không dùng ảnh để huấn luyện mô hình. Nội dung pháp lý cần được bộ phận pháp chế duyệt trước khi bật trên bản chính thức.
- Kiểm tra trạng thái: `GET /api/v1/me/wizard` trả `docAi: {"enabled": true|false, "provider": "<tên>"|null}`, không có URL và không có khoá.
