pos-app.json nằm ở thư mục gốc của dự án. POS đọc manifest khi upload để biết app cần quyền gì, được gọi domain nào và gắn vào những vị trí nào.
Ví dụ đầy đủ
{
"handle": "doi-soat-cong-no",
"name": "Đối soát công nợ",
"description": "Kiểm tra công nợ khi lên đơn và đối soát cuối ngày.",
"version": "1.3.0",
"api_version": "2026-01",
"access": {
"methods": ["GET", "POST", "PUT"],
"warehouse_ids": null,
"hidden_order_fields": ["bill_phone_number"]
},
"network": {
"allowed_origins": ["https://erp.khachhang.vn"]
},
"extensions": [
{ "target": "nav.item", "id": "nav", "label": "Đối soát", "page": "home" },
{ "target": "app.page", "id": "home", "title": "Đối soát công nợ", "render": "iframe", "entry": "page/index.html" },
{
"target": "order.details.action",
"id": "push-erp",
"label": "Đẩy sang ERP",
"presentation": "headless",
"module": "extensions/push-erp.ts",
"conditions": { "status": [1, 2] }
},
{
"target": "order.details.action",
"id": "warranty",
"label": "Tạo phiếu bảo hành",
"presentation": "modal",
"title": "Phiếu bảo hành",
"module": "extensions/warranty-modal.tsx",
"should_render": "extensions/warranty-should-render.ts"
},
{ "target": "order.details.block", "id": "debt", "module": "extensions/debt-block.tsx" },
{
"target": "purchase.index.action",
"id": "push-purchase-erp",
"label": "Đẩy sang ERP",
"presentation": "headless",
"module": "extensions/push-purchase-erp.ts",
"conditions": { "status": [1] }
},
{
"target": "order.before_submit",
"id": "check-debt",
"module": "extensions/check-debt.ts",
"timeout_ms": 2000,
"on_error": "allow"
}
]
}Trường cấp gốc
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
handle |
string | Có | Định danh app trong shop, khớp ^[a-z0-9][a-z0-9-]{1,39}$. Dùng trong URL trang của app. Đổi handle thì lần upload sau tạo một app mới, tách khỏi app cũ. |
name |
string | Có | Tên hiển thị, tối đa 60 ký tự. |
description |
string | Không | Mô tả ngắn, tối đa 300 ký tự. Hiển thị cho admin khi kích hoạt. |
version |
string | Có | Semver, ví dụ 1.3.0. Mỗi lần upload phải dùng version chưa tồn tại. |
api_version |
string | Có | Phiên bản hợp đồng của POS (YYYY-MM), quyết định danh sách target, component và schema dữ liệu ngữ cảnh. |
access |
object | Không | Giới hạn dữ liệu app được dùng. Xem access. |
network |
object | Không | Domain bên ngoài app được gọi. Xem network. |
extensions |
array | Có | Danh sách extension, tối đa 30. Xem extensions. |
access
Giới hạn thêm trên quyền của người đang dùng. App token không bao giờ có quyền vượt quá người dùng; access chỉ thu hẹp thêm.
| Trường | Kiểu | Mặc định | Mô tả |
|---|---|---|---|
methods |
string[] | ["GET"] |
HTTP method được dùng: GET, POST, PUT, DELETE. Không khai thì app chỉ đọc. |
warehouse_ids |
uuid[] | không giới hạn | Chỉ thao tác trên các kho trong danh sách. Áp dụng cho đơn hàng, phiếu nhập, phiếu xuất, phiếu chuyển kho và tồn kho theo kho. |
order_sources |
string[] | không giới hạn | Chỉ thấy đơn có nguồn trong danh sách. |
hidden_order_sources |
string[] | không ẩn | Ẩn đơn có nguồn trong danh sách. |
order_statuses |
int[] | không giới hạn | Chỉ thấy đơn có trạng thái trong danh sách. |
order_since |
datetime (ISO 8601) | không giới hạn | Chỉ thấy đơn tạo từ thời điểm này. |
hidden_order_fields |
string[] | không ẩn | Bỏ các trường này khỏi dữ liệu đơn trả về cho app, kể cả dữ liệu ngữ cảnh. |
product_categories |
int[] | không giới hạn | Chỉ thấy sản phẩm thuộc các danh mục này. |
product_brands |
int[] | không giới hạn | Chỉ thấy sản phẩm thuộc các thương hiệu này. |
Giá trị null hoặc không khai nghĩa là không giới hạn thêm (trừ methods). Giới hạn hiệu lực là access của version đang kích hoạt.
network
| Trường | Kiểu | Mô tả |
|---|---|---|
allowed_origins |
string[] | Origin https:// app được gọi bằng fetch, tối đa 10, không dùng wildcard. Open API của POS luôn được phép, không cần khai. Cũng áp dụng cho ảnh của pos-image. |
Request tới origin không có trong danh sách bị trình duyệt chặn. Server bên ngoài phải bật CORS cho domain sandbox của app (xem Truy cập dữ liệu).
extensions
Trường chung
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
target |
string | Có | Vị trí gắn extension. Xem Extension point. |
id |
string | Có | Duy nhất trong app, khớp ^[a-z0-9-]{1,40}$. |
module |
string | Tuỳ target | Đường dẫn file nguồn (.ts, .tsx, .js, .jsx). CLI build thành một ES module độc lập. Bắt buộc với target Worker UI và headless. |
Trường cho action
Áp dụng cho *.details.action, *.index.action và *.index.selection-action của mọi resource.
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
label |
string | Có | Nhãn mục menu, tối đa 40 ký tự. |
presentation |
string | Có | headless, modal, fullscreen hoặc navigate. Xem kiểu action. |
title |
string | Không | Tiêu đề modal (modal, fullscreen). Mặc định bằng label. |
page |
string | Khi navigate |
id của extension app.page cần mở. |
path |
string | Không | Đường dẫn bên trong trang khi navigate, ví dụ /returns. |
conditions |
object | Không | Điều kiện hiện mục menu, POS tự kiểm tra. Xem conditions. |
should_render |
string | Không | Module trả về true/false để quyết định có hiện mục hay không. Dùng khi conditions không đủ. |
Trường cho block
Áp dụng cho *.details.block.
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
conditions |
object | Không | Điều kiện hiện block. |
should_render |
string | Không | Như action. |
Trường cho trang và menu
| Target | Trường | Mô tả |
|---|---|---|
app.page |
title (bắt buộc) |
Tiêu đề trang. |
app.page |
render (bắt buộc) |
iframe hoặc worker (trang Worker UI dự kiến ở giai đoạn 2). |
app.page |
entry |
File HTML của trang trong gói, khi render: "iframe". Khai entry hoặc url. |
app.page |
url |
Trang tự host: địa chỉ https trên máy chủ của bạn, POS nhúng thẳng thay cho file trong gói. Không chứa tài khoản, query, #; không dùng domain của Pancake, localhost hay địa chỉ IP. Đổi url phải deploy version mới. Xem Trang tự host. |
app.page |
module |
Bắt buộc khi render: "worker". |
nav.item |
label (bắt buộc) |
Nhãn trên menu POS, tối đa 30 ký tự. |
nav.item |
page (bắt buộc) |
id của extension app.page. |
Trường cho hook
Áp dụng cho *.before_submit và *.after_save.
| Trường | Kiểu | Mặc định | Mô tả |
|---|---|---|---|
timeout_ms |
int | 2000 |
Chỉ cho before_submit, tối đa 5000. |
on_error |
string | allow |
Chỉ cho before_submit: allow cho lưu tiếp khi hook lỗi/timeout, block chặn lưu. |
conditions
Các khoá kết hợp theo AND; giá trị mảng trong một khoá kết hợp theo OR. POS kiểm tra ngay khi hiển thị nên không làm chậm menu.
| Khoá | Kiểu | Resource | Đúng khi |
|---|---|---|---|
mode |
string[] | Tất cả (target trên form chi tiết) | create (bản ghi mới chưa lưu) hoặc edit (bản ghi đã có). |
status |
int[] | order, purchase, export, warehouse_transfer |
Trạng thái bản ghi nằm trong danh sách (mã trạng thái theo Open API). |
warehouse_ids |
uuid[] | order, purchase, export, warehouse_transfer |
Kho của bản ghi nằm trong danh sách; với phiếu chuyển kho là kho đi hoặc kho đến. |
tags_any |
(int | string)[] | order, product, purchase, export |
Bản ghi có ít nhất một thẻ trong danh sách (ID thẻ). |
type |
int[] | export |
Loại phiếu xuất nằm trong danh sách. |
category_ids |
int[] | product |
Sản phẩm thuộc ít nhất một danh mục trong danh sách. |
order_sources |
string[] | order |
Nguồn đơn nằm trong danh sách. |
has_customer |
boolean | order |
Đơn đã/chưa gắn khách hàng. |
Dùng khoá không áp dụng cho resource của target là lỗi kiểm tra (invalid_condition).
Ví dụ: chỉ hiện cho đơn đã có, trạng thái 1 hoặc 2, có thẻ VIP (ID 7):
{ "mode": ["edit"], "status": [1, 2], "tags_any": [7] }Ví dụ: chỉ hiện cho phiếu nhập đã nhập hàng (trạng thái 1) vào một kho cụ thể:
{ "status": [1], "warehouse_ids": ["a52f…"] }Lỗi kiểm tra manifest
| Mã | Nguyên nhân |
|---|---|
invalid_handle |
handle sai định dạng. |
invalid_field |
Giá trị của trường không hợp lệ, ví dụ quá độ dài cho phép, khoá lạ trong access hoặc access.methods sai. |
duplicate_version |
version đã tồn tại. |
unsupported_api_version |
api_version không còn hỗ trợ hoặc không tồn tại. |
unknown_target |
target không có trong api_version đã khai. |
missing_field |
Thiếu trường bắt buộc theo target. |
module_not_found |
Không tìm thấy file module/entry/should_render. |
bundle_too_large |
Module hoặc bundle vượt giới hạn kích thước. |
storage_quota_exceeded |
Tổng dung lượng app của shop sẽ vượt 500 MB. |
invalid_bundle |
File upload không phải gói .zip hợp lệ, hoặc có đường dẫn file không an toàn. |
upload_failed |
Không lưu được bundle; thử upload lại. |
invalid_origin |
allowed_origins không phải https:// hoặc có wildcard. |
invalid_url |
url của app.page không phải https theo tên miền, có tài khoản, query, #, hoặc thuộc domain của Pancake. |
invalid_condition |
Khoá trong conditions không áp dụng cho resource của target. |
