# Kiểm tra tài liệu trong quy trình tiếp nhận hồ sơ

> Thêm bước kiểm tra tài liệu vào luồng tiếp nhận: tải lên, nhận webhook, phân luồng theo mức độ rủi ro, đưa vào hàng đợi để người xem xét, lưu mã báo cáo và xác minh sau này.

Nguồn: https://fileverity.com/vi/docs/intake-workflow

Hướng dẫn này đặt bước kiểm tra tài liệu ở đầu một quy trình nhận tài liệu từ bên ngoài, chẳng hạn hồ sơ vay, yêu cầu bồi thường bảo hiểm, tiếp nhận nhà cung cấp hoặc báo cáo chi phí. Mỗi tài liệu được kiểm tra một lần, phân luồng theo mức độ rủi ro, và mã báo cáo của tài liệu được giữ làm bản ghi kiểm toán. API trả về một mức độ và một hành động được khuyến nghị. Quyết định do chính sách của bạn và người xét duyệt đưa ra.

Hướng dẫn dùng mô hình `fvdoc-3` với các lệnh gọi trong [Tệp](https://fileverity.com/vi/docs/files.md), [Lượt phân tích](https://fileverity.com/vi/docs/analyses.md) và [Webhook](https://fileverity.com/vi/docs/webhooks.md). Với khóa thử nghiệm, bước tải lên và lượt kiểm tra trả về ngay một mẫu, nhưng không có webhook nào được gửi và không có PDF mẫu. Hãy dùng **Gửi thử** trên một endpoint webhook trong Console để thử chương trình nhận của bạn.

## Luồng xử lý

1. Tải tệp lên bằng `POST /files` và bắt đầu lượt kiểm tra bằng `POST /analyses`. Đặt mã hồ sơ của bạn vào `metadata` và gửi `Idempotency-Key`.
2. Phản hồi ngay cho người dùng của bạn. Tài liệu có thể phải xếp hàng chờ, nên đừng giữ yêu cầu của họ ở trạng thái mở; webhook sẽ cho bạn biết khi lượt kiểm tra hoàn tất.
3. Nhận `analysis.completed` hoặc `analysis.failed`, xác minh chữ ký và đưa một job vào hàng đợi.
4. Trong job, lấy lượt phân tích, phân luồng theo phần tóm tắt, rồi lưu kết quả và báo cáo PDF cùng hồ sơ.
5. Chuyển các hồ sơ cần người xử lý vào hàng đợi xem xét, kèm PDF.
6. Sau này, đối chiếu báo cáo với tệp đã lưu để xác minh.

## 1. Tải lên và bắt đầu lượt kiểm tra

Tệp có dung lượng tối đa 4 MB được tải lên dạng multipart. Với tệp lớn hơn, tối đa 50 MB, hãy dùng cách tải lên bằng URL ký trước như mô tả trong [Tệp](https://fileverity.com/vi/docs/files.md#presigned). Tạo lượt phân tích trả về `202` với `status: processing` (khóa thử nghiệm trả về `200` kèm một mẫu đã hoàn tất), và `metadata` của bạn được trả lại trên lượt phân tích cũng như trong mọi webhook của nó.

**Gửi một tài liệu: cURL**

```bash
FILE_ID=$(curl -s https://api.fileverity.com/v1/files \
  -H "Authorization: Bearer $FILEVERITY_API_KEY" \
  -H "Idempotency-Key: intake-A-10492-file" \
  -F file=@application.pdf | jq -r .id)

curl https://api.fileverity.com/v1/analyses \
  -H "Authorization: Bearer $FILEVERITY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: intake-A-10492-check" \
  -d "{\"model\": \"fvdoc-3\", \"file\": \"$FILE_ID\", \"metadata\": {\"case_id\": \"A-10492\"}}"
```

**Gửi một tài liệu: JavaScript**

```javascript
import { readFile } from "node:fs/promises";
import { basename } from "node:path";

const base = "https://api.fileverity.com/v1";
const auth = { Authorization: "Bearer " + process.env.FILEVERITY_API_KEY };

async function call(path, init) {
  const response = await fetch(base + path, init);
  const body = await response.json();
  if (!response.ok) throw new Error(body.error.code + " (" + body.error.request_id + ")");
  return body;
}

export async function submit(caseId, path) {
  const form = new FormData();
  form.append("file", new Blob([await readFile(path)]), basename(path));
  const file = await call("/files", {
    method: "POST",
    headers: { ...auth, "Idempotency-Key": `intake-${caseId}-file` },
    body: form,
  });
  return call("/analyses", {
    method: "POST",
    headers: { ...auth, "Content-Type": "application/json", "Idempotency-Key": `intake-${caseId}-check` },
    body: JSON.stringify({ model: "fvdoc-3", file: file.id, metadata: { case_id: caseId } }),
  });
}
```

**Gửi một tài liệu: Python**

```python
import os
import requests

BASE = "https://api.fileverity.com/v1"
HEADERS = {"Authorization": "Bearer " + os.environ["FILEVERITY_API_KEY"]}

def call(method, path, **kwargs):
    headers = {**HEADERS, **kwargs.pop("headers", {})}
    response = requests.request(method, BASE + path, headers=headers, timeout=90, **kwargs)
    body = response.json()
    if not response.ok:
        raise RuntimeError(body["error"]["code"] + " (" + body["error"]["request_id"] + ")")
    return body

def submit(case_id, path):
    with open(path, "rb") as source:
        file = call("POST", "/files", headers={"Idempotency-Key": f"intake-{case_id}-file"}, files={"file": source})
    return call(
        "POST",
        "/analyses",
        headers={"Idempotency-Key": f"intake-{case_id}-check"},
        json={"model": "fvdoc-3", "file": file["id"], "metadata": {"case_id": case_id}},
    )
```

**202 Accepted**

```json
{
  "id": "an_8Hk2pQ4vN9sL1xR7cW3mY6zB",
  "object": "analysis",
  "created": 1793700120,
  "model": "fvdoc-3-2026-11-03",
  "livemode": true,
  "status": "processing",
  "file": {
    "name": "application.pdf",
    "bytes": 18874368,
    "format": "pdf"
  },
  "report_id": null,
  "report_url": null,
  "summary": null,
  "result": null,
  "error": null,
  "progress": {
    "stage": "queued",
    "done": 0,
    "total": 0,
    "position": 1
  },
  "usage": {
    "amount": 599,
    "currency": "usd",
    "funding": "balance"
  },
  "metadata": {
    "case_id": "A-10492"
  },
  "completed_at": null,
  "expires_at": null
}
```

Lưu `id` cùng hồ sơ. Vì các khóa được tạo từ mã hồ sơ, việc chạy lại `submit` sau khi hết thời gian chờ sẽ trả về các phản hồi đầu tiên, thay vì tải lên tệp thứ hai hoặc tính phí lượt kiểm tra thứ hai. Khóa được lưu trong 24 giờ; xem [Idempotency](https://fileverity.com/vi/docs/idempotency.md).

## 2. Nhận kết quả

Xác minh chữ ký bằng hàm `verify` trong [Webhook](https://fileverity.com/vi/docs/webhooks.md#signature), được lưu thành `verify.js` hoặc `verify.py`. Phản hồi `2xx` thật nhanh, và thực hiện công việc trong một job. `enqueue` đại diện cho hàng đợi job của riêng bạn. Lưu mỗi job theo mã sự kiện, để một lần gửi trùng lặp không tạo ra job thứ hai.

**Chương trình nhận webhook: JavaScript**

```javascript
import express from "express";
import { verify } from "./verify.js";

const app = express();

app.post("/webhooks/fileverity", express.raw({ type: "application/json" }), async (req, res) => {
  let event;
  try {
    event = verify(process.env.FILEVERITY_WEBHOOK_SECRET, req.headers, req.body.toString("utf8"));
  } catch {
    return res.sendStatus(400);
  }
  if (event.type === "analysis.completed" || event.type === "analysis.failed") {
    await enqueue({ eventId: event.id, analysisId: event.data.id });
  }
  res.sendStatus(200);
});
```

**Chương trình nhận webhook: Python**

```python
import os

from fastapi import FastAPI, HTTPException, Request

from verify import verify

app = FastAPI()

@app.post("/webhooks/fileverity")
async def receive(request: Request):
    body = await request.body()
    try:
        event = verify(os.environ["FILEVERITY_WEBHOOK_SECRET"], request.headers, body)
    except (KeyError, ValueError):
        raise HTTPException(status_code=400)
    if event["type"] in ("analysis.completed", "analysis.failed"):
        enqueue(event["id"], event["data"]["id"])
    return {"ok": True}
```

## 3. Phân luồng theo mức độ rủi ro

`summary` là kết luận ổn định, dạng máy đọc được của một lượt kiểm tra tài liệu. `level` nhận giá trị từ `L1` đến `L5` và `action` là bước tiếp theo được khuyến nghị. Hãy phân luồng dựa trên cả hai, và coi mọi giá trị bạn không nhận ra là hồ sơ cần có người xử lý.

| Tóm tắt | Ý nghĩa | Ví dụ phân luồng |
| --- | --- | --- |
| `L1` hoặc `L2`, `accept` | Không tìm thấy dấu hiệu can thiệp. `L1` nghĩa là nguồn gốc đã được xác minh. | Tiếp tục tự động. Lưu mã báo cáo. |
| `L1` hoặc `L2`, `note` | Không có dấu hiệu can thiệp, nhưng báo cáo có đánh dấu văn bản do AI viết hoặc nhãn AI. | Tiếp tục, và gắn cờ hồ sơ nếu chính sách của bạn hạn chế nội dung do AI viết. |
| `L3`, `review` | Cần xem xét thủ công. | Hàng đợi xem xét tiêu chuẩn. |
| `L4`, `review` | Rủi ro cao. | Hàng đợi xem xét ưu tiên. |
| `L5`, `reject` | Các vấn đề được xác lập trong báo cáo. | Hàng đợi xem xét ưu tiên. Cần một người xác nhận trước khi từ chối. |
| `status` là `failed` hoặc `canceled` | Lượt kiểm tra chưa hoàn tất và bạn không bị tính phí. | Tải lên lại bản gốc đã lưu, hoặc chuyển hồ sơ cho một người xử lý. |

Cách phân luồng này là chính sách của riêng bạn. Bạn có thể chuyển `L3` vào hàng đợi ưu tiên, hoặc cho `note` đi vào hàng đợi xem xét, bằng cách thay đổi hàm. Tập hành động còn có `resubmit`, mà hàm coi là yêu cầu gửi thêm một bản khác của tệp. Mọi trường tóm tắt (summary) đều được mô tả trong [Lượt phân tích](https://fileverity.com/vi/docs/analyses.md#summary-document).

**Phân luồng một lượt kiểm tra đã hoàn tất: JavaScript**

```javascript
export function route(analysis) {
  if (analysis.status !== "succeeded") return "manual";
  const { action, level } = analysis.summary;
  if (action === "accept") return "accept";
  if (action === "note") return "accept_with_note";
  if (action === "resubmit") return "request_new_copy";
  if (action === "reject" || level === "L4" || level === "L5") return "priority_review";
  return "review";
}
```

**Phân luồng một lượt kiểm tra đã hoàn tất: Python**

```python
def route(analysis):
    if analysis["status"] != "succeeded":
        return "manual"
    action = analysis["summary"]["action"]
    level = analysis["summary"]["level"]
    if action == "accept":
        return "accept"
    if action == "note":
        return "accept_with_note"
    if action == "resubmit":
        return "request_new_copy"
    if action == "reject" or level in ("L4", "L5"):
        return "priority_review"
    return "review"
```

## 4. Lưu mã báo cáo và PDF

Job lấy lượt phân tích, phân luồng và lưu những gì bạn sẽ cần về sau. Job dùng lại `call`, `route` và thông tin xác thực từ các bước trước, còn `saveCase` và `saveReport` đại diện cho nơi lưu trữ của riêng bạn. Hãy tải PDF ngay trong cùng job: kết quả phân tích được giữ trong 7 ngày, ảnh các trang và đoạn trích văn bản của tài liệu được giữ trong 3 giờ, và sau đó Fileverity chỉ giữ bản ghi xác minh, không giữ báo cáo đầy đủ.

**Xử lý một lượt kiểm tra đã hoàn tất: JavaScript**

```javascript
export async function handleResult(analysisId) {
  const analysis = await call(`/analyses/${analysisId}`, { headers: auth });
  const decision = route(analysis);
  await saveCase(analysis.metadata.case_id, {
    analysisId: analysis.id,
    reportId: analysis.report_id,
    reportUrl: analysis.report_url,
    summary: analysis.summary,
    model: analysis.model,
    decision,
  });
  if (analysis.status === "succeeded") {
    const pdf = await fetch(`${base}/analyses/${analysis.id}/report.pdf`, { headers: auth });
    await saveReport(analysis.report_id, Buffer.from(await pdf.arrayBuffer()));
  }
  return decision;
}
```

**Xử lý một lượt kiểm tra đã hoàn tất: Python**

```python
def handle_result(analysis_id):
    analysis = call("GET", f"/analyses/{analysis_id}")
    decision = route(analysis)
    save_case(analysis["metadata"]["case_id"], {
        "analysis_id": analysis["id"],
        "report_id": analysis["report_id"],
        "report_url": analysis["report_url"],
        "summary": analysis["summary"],
        "model": analysis["model"],
        "decision": decision,
    })
    if analysis["status"] == "succeeded":
        pdf = requests.get(f"{BASE}/analyses/{analysis['id']}/report.pdf", headers=HEADERS, timeout=90)
        pdf.raise_for_status()
        save_report(analysis["report_id"], pdf.content)
    return decision
```

- `report_id` (`FV-XXXX-XXXX-XXXX`) là mã dùng để tra cứu bản ghi kiểm toán. Hãy lưu mã này cùng hồ sơ, trong cơ sở dữ liệu của bạn và trên mọi thư thông báo quyết định.
- `model` là phiên bản có gắn ngày (snapshot) đã tạo ra kết quả, để về sau bạn có thể nói chính xác phiên bản nào đã kiểm tra tài liệu.
- Hãy giữ tệp gốc trong nơi lưu trữ của riêng bạn. Fileverity xóa tệp đã tải lên khi lượt phân tích bắt đầu, và một lượt kiểm tra thất bại cần tải lên lại. Xem [Lưu giữ dữ liệu và bảo mật](https://fileverity.com/vi/docs/security.md).

## 5. Vận hành hàng đợi xem xét thủ công

Tạo một tác vụ cho mỗi hồ sơ đã phân luồng. Cung cấp cho người xét duyệt những gì họ cần để quyết định mà không phải gọi lại API:

- Mã hồ sơ, `level`, `action`, `reason` và các mã phát hiện trong `findings`, kèm báo cáo PDF. PDF liệt kê từng phát hiện cùng vị trí và bằng chứng của nó.
- Tệp gốc bạn đã lưu.
- `report_url`, trang xác minh công khai. Trang này xác nhận bản ghi báo cáo và giá trị băm của tệp, nhưng không thay thế việc đọc báo cáo.

Ghi lại người xét duyệt, quyết định và thời điểm trong hệ thống của riêng bạn, bên cạnh `report_id`.

> **Lưu ý.** Một lượt kiểm tra không phát hiện vấn đề không phải là bằng chứng cho thấy tài liệu là thật, và một phát hiện không phải là bằng chứng của gian lận. Đừng từ chối một hồ sơ chỉ dựa vào kết quả của API. Với tài liệu về thu nhập, danh tính hoặc quyền sở hữu, hãy xác nhận với đơn vị phát hành qua một đầu mối liên hệ do chính bạn tìm được. [Hướng dẫn phát hiện gian lận tài liệu](https://fileverity.com/vi/huong-dan/nhan-biet-tai-lieu-gia-mao-do-ai-tao-ra) trình bày những điểm cần kiểm tra theo từng loại tài liệu.

## 6. Xác minh báo cáo sau này

Nhiều tháng sau, kiểm toán viên hoặc khách hàng có thể kiểm tra bản ghi mà không cần tài khoản Fileverity. Hãy mở `report_url` đã lưu. Trang này hiển thị loại báo cáo, phát hiện và bước tiếp theo đã được ghi nhận, giá trị băm SHA-256 của tệp đã được kiểm tra và dấu thời gian RFC 3161. Thả bản gốc đã lưu của bạn vào trang, trình duyệt sẽ tính SHA-256 của tệp và so sánh với bản ghi; không có gì được tải lên. Mọi bản đã lưu lại, chuyển đổi hoặc chỉnh sửa đều cho giá trị băm khác.

Bạn có thể chạy cùng phép so sánh này trong hệ thống của riêng mình:

**Tính giá trị băm của bản gốc đã lưu: cURL**

```bash
sha256sum application.pdf
```

**Tính giá trị băm của bản gốc đã lưu: JavaScript**

```javascript
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";

const digest = createHash("sha256").update(await readFile("application.pdf")).digest("hex");
console.log(digest);
```

**Tính giá trị băm của bản gốc đã lưu: Python**

```python
import hashlib

print(hashlib.sha256(open("application.pdf", "rb").read()).hexdigest())
```

Việc xác minh không phân tích lại tệp và không chứng nhận rằng tài liệu là xác thực. Nó ghi lại tệp nào đã được kiểm tra, báo cáo nói gì và vào thời điểm nào. Báo cáo đầy đủ chỉ tồn tại trong PDF bạn đã lưu. Các trường của báo cáo được mô tả trong [đối tượng báo cáo](https://fileverity.com/vi/docs/analyses.md#report).

## Trước khi triển khai chính thức

- Hãy xây dựng bằng khóa thử nghiệm. Khóa này trả về ngay một mẫu, mức `L2` với hành động `accept`, mà không chạy mô hình. Webhook và việc tải PDF cần khóa thật, mà bạn có thể thử bằng lượt dùng thử miễn phí. Hãy đưa các tóm tắt khác trong [Lượt phân tích](https://fileverity.com/vi/docs/analyses.md#summary-document) qua `route` để kiểm thử từng nhánh.
- Xử lý `503 service_unavailable`: chờ số giây trong `retry-after` rồi thử lại với cùng `Idempotency-Key`. Xem [Khi dịch vụ đang bận](https://fileverity.com/vi/docs/analyses.md#busy).
- Đặt hạn mức chi tiêu hằng tháng cho khóa và thêm webhook `balance.low`, để quy trình tiếp nhận không bị dừng với lỗi `insufficient_balance`. Xem [Giá và thanh toán](https://fileverity.com/vi/docs/pricing.md#alerts).
- Hãy gọi API từ máy chủ của bạn và giữ khóa trong trình quản lý bí mật (secrets manager), không bao giờ để trong mã chạy trên trình duyệt.
- Hãy cung cấp giá trị header `x-request-id` khi bạn [liên hệ hỗ trợ](https://fileverity.com/vi/lien-he).

