# Truy cập dữ liệu

App đọc/ghi dữ liệu shop qua **Open API hiện có** ([tài liệu Open API](/developers/api)): cùng endpoint, format response, phân trang và quy tắc cập nhật. Code đang chạy với API key chỉ cần đổi cách lấy credential.

```ts
// Trước: key cố định, toàn quyền, dễ lộ khi nằm trong code frontend
fetch(`https://pos.pages.fm/api/v1/shops/${SHOP_ID}/orders?api_key=${STATIC_KEY}`);

// Trong app: SDK tự gắn app token
await pos.api.get(`/shops/${pos.shop.id}/orders`);
```

## App token

POS tự cấp app token khi extension chạy; app không cần và không nên có API key cố định.

| | Shop API Key | User API Key | **App token** |
|---|---|---|---|
| Cấp bởi | Admin tạo, dùng lâu dài | User tạo, dùng lâu dài | POS cấp tự động, hết hạn sau 15 phút |
| Danh tính | Tài khoản kết nối của shop | User thật | User thật + app |
| Quyền | Toàn quyền (có thể giới hạn) | Quyền của user | **Quyền của user ∩ `access` của app** |
| Phạm vi shop | Một shop | Mọi shop user tham gia | Chỉ shop đang mở |
| Nơi dùng | Server | Server, script | Bên trong app trên POS |

- Token gắn với một shop, một user, một app và version đang chạy.
- SDK gửi token qua header `Authorization: Bearer <token>`. Tham số `?api_key=<token>` vẫn được chấp nhận để tương thích code Open API cũ, nhưng không khuyến nghị vì URL dễ lọt vào log.
- Token bị thu hồi ngay khi: app bị tắt hoặc gỡ, admin đổi `access`, user đăng xuất hoặc bị đổi phiên đăng nhập. User bị xóa khỏi shop hoặc đổi role thì có hiệu lực từ request kế tiếp.

## Quyền hiệu lực

```text
Quyền của app token = quyền của user đang dùng ∩ access của app (bản admin đã duyệt)
```

**Quyền của user** áp dụng giống hệt khi user thao tác trên POS: role, quyền theo kho, quyền theo bảng/trường CRM, bộ phận, giờ làm việc, IP được phép, module đang bật và gói dịch vụ.

**`access` của app** thu hẹp thêm: method được dùng (mặc định chỉ `GET`), kho, nguồn đơn, trạng thái đơn, thời điểm tạo đơn, danh mục/thương hiệu sản phẩm, trường đơn bị ẩn. Xem [Manifest › access](/developers/apps/manifest#access).

Hệ quả:

- Cùng một app, người có quyền rộng thấy nhiều dữ liệu hơn người có quyền hẹp.
- Request chạy được bằng Shop API Key có thể bị từ chối với app token. Đây là hành vi đúng.
- App nên xử lý lỗi `403` thay vì giả định mọi người dùng đều có quyền.

### Nhóm luôn bị chặn

Các nhóm sau chỉ dùng được từ phiên đăng nhập trên giao diện POS, mọi loại API key và app token đều bị từ chối:

- Quản lý nhân sự: thêm/sửa nhân viên, phân quyền.
- Liên kết tài khoản Google.
- Quản lý API key.
- Quản lý app và cấp app token.

## Endpoint được gọi

- Chỉ dùng các endpoint có trong [tài liệu Open API](/developers/api). Endpoint nội bộ của giao diện POS có thể đổi bất kỳ lúc nào và sẽ bị chặn với app token.
- Mọi đường dẫn phải thuộc shop hiện tại: `/shops/{shop_id}/...`.
- Thiếu dữ liệu hoặc thao tác cần thiết: gửi yêu cầu bổ sung vào Open API thay vì dùng endpoint nội bộ.

Endpoint chính của các resource có extension point:

| Resource | Open API | Mã bản ghi trong đường dẫn |
|---|---|---|
| `order` | `GET`/`POST /shops/{shop_id}/orders`, `GET`/`PUT /shops/{shop_id}/orders/{id}` | `record.system_id` |
| `product` | `GET /shops/{shop_id}/products/variations`, `POST /shops/{shop_id}/products`, `GET`/`PUT /shops/{shop_id}/products/{id}` | `record.id` |
| `purchase` | `GET`/`POST /shops/{shop_id}/purchases`, `GET`/`PUT /shops/{shop_id}/purchases/{id}` | `record.id` |
| `export` | `GET`/`POST /shops/{shop_id}/export`, `GET`/`PUT /shops/{shop_id}/export/{id}` | `record.id` |
| `warehouse_transfer` | `GET /shops/{shop_id}/transfers`, `POST /shops/{shop_id}/transfers/multi`, `GET`/`PUT /shops/{shop_id}/transfers/{id}` | `record.id` |

## Dữ liệu ngữ cảnh

`pos.data` chứa dữ liệu tối thiểu POS đang hiển thị (ví dụ mã và trạng thái đơn hay phiếu nhập đang mở) để extension không phải gọi API ngay khi khởi động. Dữ liệu này đã được lọc theo `access` của app. Cần thêm dữ liệu thì gọi Open API.

## Giới hạn tần suất

- Tính theo **app + user + shop + endpoint**; nhiều extension của cùng app dùng chung hạn mức: dưới 30 lượt đọc (`GET`) và dưới 100 lượt ghi mỗi giây.
- Vượt giới hạn: HTTP `429`. Thử lại sau, tăng dần thời gian chờ (ví dụ 1 s, 2 s, 4 s), tối đa 3 lần.
- Không gọi API trong vòng lặp render. Gom request, dùng phân trang và `AbortSignal` để huỷ request cũ khi người dùng đổi bộ lọc.

## Audit

Mọi thao tác ghi bằng app token được lưu với **user thật**, **app** và **version**. Các trường tự động như người tạo, người sửa ghi theo user đang dùng.

## Mã lỗi

Cột `code` là trường `error_code` trong body, SDK đưa vào `PosApiError.code`.

| HTTP | `code` | Nghĩa | SDK xử lý |
|---|---|---|---|
| 401 | `111` | Token hết hạn hoặc bị thu hồi: app bị tắt, kích hoạt version khác, đổi bộ phận được thấy app, user đổi phiên đăng nhập (xem [App token](#app-token)) | Lấy token mới, thử lại 1 lần. Vẫn lỗi thì ném `PosApiError` (`status: 401`, `code: 111`); POS từ chối cấp token mới (app bị tắt hoặc user không còn được thấy app) thì ném `PosRuntimeError` với `code: 'failed'` |
| 403 | | Không đủ quyền: role, kho, giờ làm, IP, nhóm luôn bị chặn | Ném `PosApiError` |
| 403 | `110` | Endpoint không có trong [Open API](/developers/api) | Ném `PosApiError` |
| 404 | | Không tìm thấy, hoặc dữ liệu nằm ngoài `access` của app | Ném `PosApiError` |
| 404 | | Method không có trong `access.methods` (nếu gửi trực tiếp không qua SDK) | SDK chặn trước khi gửi |
| 429 | `106` | Vượt giới hạn tần suất | Ném `PosApiError`; app tự thử lại |
| 200 | tuỳ endpoint | Body có `success: false`: lỗi nghiệp vụ, xem `message` | Ném `PosApiError` với `status: 200` |

## Gọi hệ thống bên ngoài

- Khai origin trong `network.allowed_origins`; request tới origin khác bị chặn.
- Server bên ngoài phải bật CORS cho origin sandbox của app, là header `Origin` trong request app gửi tới khi chạy bản đã kích hoạt. Ở chế độ phát triển, origin là địa chỉ bạn nhập khi bật (ví dụ `http://localhost:5174`); muốn thử thì bật CORS cho cả origin này.
- **Không gửi app token ra ngoài**, trừ backend của [trang tự host](/developers/apps/runtime-api#trang-tu-host). Hệ thống bên ngoài cần dữ liệu POS thì app đọc qua Open API rồi gửi phần cần thiết.
- **Không đặt secret trong code app.** Mọi người dùng mở được app đều tải được bundle. Cần gọi hệ thống có secret thì đặt một backend trung gian của shop, xác thực người gọi bằng cách riêng.

## Tác vụ chạy nền

App chỉ chạy khi có người đang mở POS. Tác vụ định kỳ, đồng bộ hoặc xử lý sự kiện khi không ai mở POS cần backend riêng của shop:

- Gọi Open API bằng **User API Key** (khuyến nghị, quyền theo user) hoặc **Shop API Key** như hiện nay.
- Nhận sự kiện thay đổi qua webhook của shop (Cấu hình › Webhook - API).

Dự kiến giai đoạn 2: **service key theo app** (chạy dưới danh nghĩa một user được chỉ định, áp `access` của app) và **webhook đăng ký theo app** có ký HMAC.

## Dữ liệu riêng của app

Phiên bản này chưa có kho lưu trữ riêng cho app. Lựa chọn:

- Bảng CRM tuỳ biến của shop, đọc/ghi qua Open API: có sẵn phân quyền theo bảng và trường.
- Backend riêng của shop.
- `localStorage` trong trang iframe: chỉ lưu trên trình duyệt hiện tại, phù hợp cho tuỳ chọn giao diện.
