Skip to main content

Chia sẻ thẻ & tự link thẻ

Trang này dành cho ba nhóm FE:

FEPhần cần đọc
Corporate portal — admin chia sẻ thẻ cho nhân viên§2
App xperise / XPay — nhân viên dùng thẻ được chia sẻ§3
App hoặc trang web mở từ Zalo OA — người dùng tự link thẻ§4

Trang web công khai từ Zalo OA — không đăng nhập, xác thực SĐT bằng OTP rồi link thẻ — có trang riêng: Link thẻ bằng OTP SĐT.

Trang này mô tả cái đang có, không phải cái nên có

Mọi field, quyền và mã lỗi ở đây đọc thẳng từ code card-core. Chỗ nào chưa có được gom ở §6 — Chưa có.

Base URL:

Môi trườngURL
Staginghttps://api-staging-core-card.uts.network
Localhttp://localhost:3000

Spec máy đọc được: docs/openapi.generated.json trong repo card-core.


1. Mô hình​

Mọi thẻ đều là thẻ bearer: thẻ của công ty, holder (cards.user_id) không phải một nhân viên cụ thể. Một nhân viên muốn trả bằng thẻ thì thẻ phải được chia sẻ cho họ.

  • Nhiều–nhiều: một thẻ chia sẻ cho nhiều nhân viên, một nhân viên giữ nhiều thẻ.
  • Có ba cách để một người được link vào thẻ:
    • Admin chia sẻ (§2): corporate admin hoặc platform.
    • Tự link (§4): người dùng chứng minh mình cầm thẻ bằng số thẻ đầy đủ + PIN in kèm thẻ.
    • Tổng đài / ops link giúp: chọn thẻ của công ty khách, hoặc nhập số thẻ + PIN khách đọc qua điện thoại. Xem Thanh toán chuyến xe bằng thẻ.
ViệcHolder / người được link
Thấy thẻ trong GET /my/cards, xem số dư, hạn mức✅ / ✅
Tạo mã QR một lần (POST /my/cards/{card_id}/tokens)✅ / ✅
Xác nhận QR order của merchant (POST /payment-intents/{intent_id}/confirm)✅ / ✅
Chọn thẻ để trả chuyến xe đặt qua app (POST /my/rides)✅ / ✅
Xem chính sách thẻ (GET /my/cards/{card_id}/policy)✅ / ✅
Xem PAN, kích hoạt, khoá, đổi PIN, đổi cách xác thực✅ / ❌ 404

Các thao tác "của chủ thẻ" trả 404 not_found cho người được link, không phải 403, để không lộ việc thẻ tồn tại.

Ai đã tiêu: acting_user_id​

Mỗi giao dịch giữ hai người:

FieldNghĩa
user_idHolder của thẻ — không đổi
acting_user_idNgười thực sự trả trong app: người tạo mã QR, xác nhận QR order hoặc đi chuyến xe. null khi không biết ai (quẹt/chạm thẻ vật lý ở POS, s2s)

GET /my/payments của một nhân viên trả giao dịch trên thẻ của chính họ cộng giao dịch do chính họ thực hiện trên thẻ được chia sẻ — không bao giờ thấy giao dịch của người dùng chung khác.


2. Corporate portal — chia sẻ thẻ cho nhân viên​

2.1 Quyền​

CredentialHeaderPhạm vi
Corporate adminAuthorization: <raw corporate access token> (không prefix Bearer)Chỉ thẻ của corporate mình; thẻ corporate khác → 404
Platform keyX-API-Key: <PLATFORM_API_KEY>Mọi thẻ. Chỉ backend giữ — không để lộ ra trình duyệt

System admin (token Store) không gọi được nhóm route /cards/* này. Tổng đài viên dùng route riêng bên dispatch — xem Thanh toán chuyến xe bằng thẻ.

2.2 Endpoint​

MethodPathMô tả
GET/cards/{card_id}/usersDanh sách nhân viên đang được chia sẻ thẻ, kèm hạn link
POST/cards/{card_id}/usersChia sẻ thẻ cho một nhân viên, có thể kèm hạn link (§2.3)
PATCH/cards/{card_id}/users/{user_id}Đổi hạn link (§2.3)
DELETE/cards/{card_id}/users/{user_id}Ngừng chia sẻ

Chia sẻ thẻ

Request
POST /cards/{card_id}/users
Authorization: <corporate access token>
Content-Type: application/json

{
"user_id": "4242",
"windows": [
{ "valid_from": "2026-11-01T00:00:00+07:00", "valid_until": "2026-11-06T00:00:00+07:00" },
{ "valid_from": "2026-11-20T00:00:00+07:00", "valid_until": "2026-11-26T00:00:00+07:00" }
]
}
FieldTypeRequiredMô tả
user_idstring (chữ số)✅User id (Store) của nhân viên
windowsmảng window—Các khoảng nhân viên được trả bằng thẻ. Bỏ trống hoặc [] = trả lúc nào cũng được. Luật ở §2.3
201 Created
{
"card_id": "f4eedbf6-97e7-4ac9-934c-f1cce01cce98",
"user_id": "4242",
"name": "Lê Hoàng Tuấn",
"phone": "+84901234567",
"email": "tuan.le@acme.vn",
"linked_by": "1093",
"linked_by_type": "corporate_admin",
"windows": [
{ "valid_from": "2026-10-31T17:00:00.000Z", "valid_until": "2026-11-05T17:00:00.000Z" },
{ "valid_from": "2026-11-19T17:00:00.000Z", "valid_until": "2026-11-25T17:00:00.000Z" }
],
"link_status": "not_started",
"created_at": "2026-10-30T03:00:00.000Z"
}
  • Corporate admin: backend kiểm user_id là nhân viên active của corporate qua backend corporate, bằng chính token của admin, rồi lưu cache thông tin nhân viên (tên, SĐT).
  • Platform key: được tin, chỉ từ chối khi user_id đã được biết là nhân viên của corporate khác.
  • Gọi lại nhiều lần an toàn: link đã có thì trả về link cũ. Nhưng gọi lại với windows khác hạn đang có thì bị 409 card_user_exists. Muốn đổi hạn thì dùng PATCH (§2.3).
  • name lấy từ cache nhân viên, họ trước tên sau. null khi chưa có.
  • phone (E.164) và email lấy từ cache nhân viên, và chỉ có khi đó là nhân viên của chính corporate sở hữu thẻ. Còn lại là null.
  • GET /cards/{card_id}/users trả mảng các phần tử cùng shape như trên.
  • Thời gian trả về luôn ở UTC (…Z). FE tự đổi sang giờ Việt Nam để hiển thị.

2.3 Hạn dùng (windows)​

Có hai lớp hạn, đều do admin đặt và đều là danh sách các khoảng thời gian (window):

LớpLưu ởĐặt quaÁp cho
Hạn thẻcard_validity_windowsvalidity_windows trên POST /cards, POST /physical-cards/activate, PATCH /cards/{card_id}Mọi người trả bằng thẻ: holder, người được link, quẹt/chạm ở POS, s2s
Hạn linkcard_user_windowswindows trên POST /cards/{card_id}/users (§2.2) và PATCH /cards/{card_id}/users/{user_id}Chỉ một nhân viên được link

Một window:

{ "valid_from": "2026-11-01T00:00:00+07:00", "valid_until": "2026-11-06T00:00:00+07:00" }
FieldTypeMô tả
valid_fromdate-time có UTC offset, hoặc nullTừ lúc này. null hoặc bỏ trống = không giới hạn đầu
valid_untildate-time có UTC offset, hoặc nullTới lúc này, không tính chính thời điểm đó. null hoặc bỏ trống = không có hạn cuối. Phải sau valid_from

Luật chung cho cả hai lớp. Sai luật thì trả 422 validation_failed và không ghi gì:

  • Mỗi window phải có ít nhất một đầu. Muốn bỏ giới hạn thì gửi danh sách rỗng, không gửi window {}.
  • Thời gian bắt buộc có offset: 2026-11-01T00:00:00+07:00 hoặc …Z. Gửi 2026-11-01 hay 2026-11-01T00:00:00 (không offset) bị 422.
  • Window đã kết thúc bị bỏ khi ghi. Vì vậy FE gửi lại nguyên danh sách GET trả về, thêm window mới, mà không cần tự lọc window cũ. Nếu mọi window gửi lên đều đã qua, backend giữ window kết thúc muộn nhất để danh sách vẫn là "hết hạn", không biến thành "không giới hạn".
  • Tối đa 20 window chưa kết thúc, và tối đa 100 window mỗi request (tính cả window đã qua).
  • Các window không được chồng nhau. Kề nhau thì được: một window kết thúc đúng lúc window sau bắt đầu.
  • Đây là các khoảng rời nhau, không phải lịch lặp (kiểu "thứ 2–6, 8h–17h").

Đổi hạn link

Request
PATCH /cards/{card_id}/users/{user_id}
Authorization: <corporate access token>
Content-Type: application/json

{ "windows": [ { "valid_from": "2026-12-01T00:00:00+07:00", "valid_until": null } ] }
  • windows bắt buộc và thay toàn bộ hạn link. [] = bỏ giới hạn.
  • Trả về link cùng shape với POST (§2.2), 200. Link không tồn tại → 404 not_found.
  • Thêm một window phía trước cho link đã expired thì nhân viên trả được lại trong window đó.

Hạn thẻ (thẻ gốc)

Hạn thẻ gắn trên chính thẻ, nên áp cho mọi người trả bằng thẻ, kể cả holder. Mọi người được link đều chịu chung hạn này, cộng thêm hạn link riêng của từng người.

LúcRequestAi gọi
Tạo thẻ ảoPOST /cards với validity_windowsCorporate admin
Kích hoạt thẻ vật lýPOST /physical-cards/activate với validity_windowsCorporate admin
SửaPATCH /cards/{card_id} với validity_windowsCorporate admin hoặc platform key
Request
PATCH /cards/{card_id}
Authorization: <corporate access token>
Content-Type: application/json

{
"validity_windows": [
{ "valid_from": "2026-11-03T00:00:00+07:00", "valid_until": "2026-11-11T00:00:00+07:00" }
]
}
200 (rút gọn)
{
"card_id": "f4eedbf6-97e7-4ac9-934c-f1cce01cce98",
"status": "active",
"expires_at": "2028-10-31T16:59:59.000Z",
"validity": {
"status": "not_started",
"windows": [
{ "valid_from": "2026-11-02T17:00:00.000Z", "valid_until": "2026-11-10T17:00:00.000Z" }
]
}
}
  • PATCH có gửi validity_windows thì thay toàn bộ hạn thẻ, [] = bỏ giới hạn. Không gửi field này thì giữ nguyên. Cùng luật window như trên.
  • validity có trên mọi response thẻ: POST /cards, PATCH /cards/{card_id}, GET /cards, GET /cards/{card_id}, và GET /my/cards phía app (§3.1).
  • Đổi hạn thẻ không đụng tới hạn link của ai.
Hạn thẻ khác ngày hết hạn và trạng thái thẻ

Thẻ có ba điều kiện riêng, cái nào không qua cũng không trả được:

Điều kiệnFieldKhông qua thì declineAi sửa
Trạng tháistatusTheo trạng thái thẻ (khoá, chưa kích hoạt…)Route activate / block / unblock
Ngày hết hạnexpires_atcard_expiredThẻ ảo: PATCH expires_at. Thẻ vật lý: không sửa được (in trên thẻ và trong chip) — 422 expiry_issuer_owned
Hạn thẻvalidity_windowscard_outside_validity_windowPATCH validity_windows
  • validity.status chỉ tính theo window. Thẻ đã quá expires_at hoặc đang khoá vẫn có thể có validity.status = "active". FE phải xét cả ba.
  • Window kéo dài quá expires_at không gia hạn thẻ. Sau expires_at thẻ bị decline card_expired, dù window còn.

Link bằng PIN không có hạn. Tự link (§4), link qua trang Zalo OA và tổng đài link giúp đều tạo link không giới hạn. Chúng cũng không bao giờ đổi hạn admin đã đặt cho một link có sẵn. Chỉ API admin ở trên đặt được hạn link.

Trạng thái​

Thẻ có validity: { status, windows }, link có link_status + windows. Cả hai cùng dùng ba giá trị:

statusNghĩa
activeKhông có window nào (không giới hạn), hoặc đang ở trong một window. Chỉ trạng thái này mới trả được
not_startedKhông ở trong window nào, nhưng còn ít nhất một window phía trước: trước window đầu, hoặc đang ở giữa hai window
expiredMọi window đã qua

Ngoài hạn, thẻ vẫn hiện ở mọi danh sách (portal lẫn app), chỉ là không trả được. Thẻ chỉ biến khỏi app của nhân viên khi xoá link.

Trên portal:

  • GET /cards trả validity của từng thẻ và linked_users: [{ user_id, name, windows, link_status }].
  • GET /cards/{card_id} trả validity.
  • GET /cards/{card_id}/users trả windows và link_status của từng link.

Hai lớp hạn cùng lúc​

Một nhân viên được link trả được bằng thẻ khi cả hai lớp đều active:

trả được = thẻ status "active" VÀ chưa quá expires_at VÀ validity.status = "active" VÀ link.status = "active"

Holder (thẻ của chính họ) chỉ chịu hạn thẻ.

Backend không bắt hạn link nằm trong hạn thẻ

Hai lớp được lưu và kiểm độc lập. Admin có thể lưu một hạn link nằm ngoài hạn thẻ, và backend không báo lỗi. Thực tế nhân viên chỉ trả được ở phần giao nhau:

Hạn thẻHạn linkNhân viên trả được
03/11 – 10/11không giới hạn03/11 – 10/11
không giới hạn01/11 – 05/1101/11 – 05/11
03/11 – 10/1101/11 – 05/1103/11 – 05/11
03/11 – 10/1120/11 – 25/11Không lúc nào — link active từ 20/11 nhưng thẻ đã expired

Portal nên tự so hai lớp và cảnh báo admin khi một window của link nằm ngoài mọi window của thẻ. Đổi hạn thẻ không đụng tới hạn link, và ngược lại.

Từng đường trả tiền xử lý hai lớp như sau:

Đường trảNgoài hạn linkNgoài hạn thẻ
Tạo mã QR (POST /my/cards/{card_id}/tokens)403 card_link_inactive422 card_outside_validity_window
Mã QR đã tạoSống tối đa tới cuối window nào kết thúc sớm hơn (thẻ hoặc link) đang chứa lúc tạo. Admin rút hạn hay xoá link sau đó thì merchant quét bị decline card_link_inactiveMerchant quét bị decline card_outside_validity_window
Xác nhận QR order (POST /payment-intents/{intent_id}/confirm)403 card_link_inactiveGiao dịch bị decline card_outside_validity_window
Quẹt/chạm thẻ, nhập số thẻ ở POS, s2sKhông áp (không biết ai đang cầm thẻ)Decline card_outside_validity_window
Chọn thẻ khi đặt xe (POST /my/rides)422 invalid_payment_card422 invalid_payment_card
Trừ thẻ lúc hoàn tất chuyến, thẻ đã chọn lúc đặt (payment_card_id)Không kiểm lại — đặt xe trong hạn link thì vẫn trừ dù hạn đã hết, miễn link chưa bị xoáKiểm lại — hạn thẻ đã hết thì 409 payment_declined, reason = card_outside_validity_window, ticket vẫn mở
Trừ thẻ lúc hoàn tất chuyến, không chọn thẻLúc hoàn tất mới chọn thẻ active duy nhất đang trong cả hai lớp hạn. Không còn thẻ nào thì 409 no_payment_cardNhư cột bên trái
Nhận OTP của thẻNhân viên ngoài hạn link không có trong danh sách người nhận OTPKhông đổi danh sách người nhận, nhưng giao dịch vẫn bị decline

Ngừng chia sẻ

DELETE /cards/{card_id}/users/{user_id} → 200 { "card_id": "…", "user_id": "4242", "unlinked": true }

Ngừng chia sẻ làm hết hạn ngay các mã QR mà nhân viên đó đã tạo trên thẻ mà chưa dùng (mã có PIN sống tới 7 ngày). Giao dịch cũ vẫn nằm trong lịch sử của họ.

2.4 Mã lỗi​

HTTPcodeKhi nào
404not_foundThẻ không có / không thuộc corporate của admin; PATCH hoặc DELETE một người chưa được link
422validation_faileduser_id sai định dạng, hoặc là chính holder của thẻ. Hoặc windows / validity_windows sai luật §2.3: window không có đầu nào, thiếu UTC offset, valid_until không sau valid_from, chồng nhau, quá 20 window chưa kết thúc, quá 100 window mỗi request
409card_user_existsPOST lại cho người đã được link, với windows khác hạn đang có. details.windows = hạn hiện tại. Dùng PATCH
422user_not_in_corporateKhông phải nhân viên active của corporate (admin), hoặc là nhân viên corporate khác (platform)
503 / 502admin_auth_unavailable / corporate_upstream_errorKhông hỏi được backend corporate

2.5 Sự kiện​

Qua outbox (GET /events, topic card-core.cards):

event_typePayload
card.user_linked{ card_id, corporate_id, user_id, linked_by: { id, type }, via, windows } — via = admin (§2) hoặc card_pin (tự link, §4); windows = hạn link lúc tạo ([] = không giới hạn)
card.user_link_updated{ card_id, corporate_id, user_id, windows, updated_by: { id, type } } — chỉ phát khi hạn link thực sự đổi
card.user_unlinked{ card_id, corporate_id, user_id, unlinked_by: { id, type } }
card.validity_updated{ card_id, corporate_id, validity_windows } — chỉ phát khi PATCH làm hạn thẻ thực sự đổi
card.issuedTạo thẻ ảo (POST /cards): có validity_windows. Kích hoạt thẻ vật lý: không có validity_windows (chỉ form_factor: "physical") — muốn biết hạn thì đọc GET /cards/{card_id}

Portal nên dựa vào via = card_pin để báo admin "có người vừa tự link thẻ X", vì đó là lần duy nhất admin biết.


3. App — dùng thẻ được chia sẻ​

Header: Authorization: <raw Store access token của user>.

3.1 GET /my/cards, GET /my/cards/{card_id}​

Mỗi thẻ có thêm field access:

accessNghĩaApp hiện
ownerThẻ của chính userĐủ chức năng
linkedThẻ được chia sẻ cho userẨn xem PAN, kích hoạt, khoá, đổi PIN, đổi cách xác thực

Và hai field về hạn (§2.3):

FieldCó ởNghĩa
validity: { status, windows }Mọi thẻHạn thẻ. Áp cho cả holder
link: { status, windows }Thẻ linked. null trên thẻ của chính userHạn link của chính user
Một thẻ được chia sẻ, đang giữa hai window của link
{
"card_id": "f4eedbf6-97e7-4ac9-934c-f1cce01cce98",
"access": "linked",
"status": "active",
"validity": { "status": "active", "windows": [] },
"link": {
"status": "not_started",
"windows": [
{ "valid_from": "2026-10-31T17:00:00.000Z", "valid_until": "2026-11-05T17:00:00.000Z" },
{ "valid_from": "2026-11-19T17:00:00.000Z", "valid_until": "2026-11-25T17:00:00.000Z" }
]
}
}

Thẻ trả được khi status = "active", chưa quá expires_at, validity.status = "active" và link là null hoặc link.status = "active". Ngoài hạn, thẻ vẫn nằm trong danh sách. App nên làm mờ thẻ, chặn nút trả tiền và ghi lý do:

Trường hợpGợi ý hiển thị
link.status = "not_started" hoặc validity.status = "not_started""Dùng được từ …" — lấy valid_from của window kế tiếp
link.status = "expired" hoặc validity.status = "expired""Đã hết hạn" — hướng dẫn liên hệ admin công ty

Cả hai lớp đều có window phía trước thì thẻ chỉ dùng được lại khi cả hai cùng active: FE tính phần giao nhau của hai danh sách window. Một lớp expired thì thẻ không dùng được nữa, dù lớp kia còn window phía trước.

3.2 Trả tiền​

Không có gì khác với thẻ của mình:

  • Mã QR: POST /my/cards/{card_id}/tokens. Mã ghi lại người tạo, nên giao dịch merchant quét mã đó có acting_user_id = user.
  • QR order của merchant: POST /payment-intents/{intent_id}/confirm với card_id của thẻ được chia sẻ.
  • Chuyến xe: xem Thanh toán chuyến xe bằng thẻ.

Ngoài hạn thì từng đường trả báo lỗi khác nhau: 403 card_link_inactive (hạn link), 422 card_outside_validity_window (hạn thẻ), 422 invalid_payment_card (chuyến xe). Bảng đầy đủ ở cuối §2.3.

3.3 GET /my/payments​

Trả giao dịch trên thẻ của user cộng giao dịch user thực hiện trên thẻ được chia sẻ. Lọc theo card_id vẫn dùng như cũ.


Người dùng tự thêm thẻ vào tài khoản: có số thẻ đầy đủ và PIN in kèm thẻ là đủ quyền, giống như ở quầy merchant. Không cần corporate admin chia sẻ trước.

4.1 Endpoint​

Request
POST /my/cards/link
Authorization: <raw Store access token của user>
Content-Type: application/json

{ "pan": "9686860012345678", "pin": "246810" }
FieldTypeRequiredMô tả
panstring, 16 chữ số✅Số thẻ in trên mặt thẻ
pinstring, 4–8 chữ số✅PIN in kèm thẻ

200 — thẻ, cùng shape với GET /my/cards/{card_id}, access: "linked". Nếu user chính là holder thì access: "owner" và không link gì thêm.

4.2 Chống dò đoán​

LuậtHệ quả
Sai số thẻ và sai PIN trả cùng một lỗiKhông dò được số thẻ nào có thật
3 lần sai PIN trên một thẻ trong 15 phút (tính từ lần đúng gần nhất)Thẻ khoá 15 phút. Khoá này dùng chung với luồng nhập PIN ở merchant
Thẻ đang khoáTừ chối ngay, không so PIN
5 lần sai của một user trong 1 giờ, tính mọi thẻ kể cả số thẻ không tồn tạiUser bị tạm chặn cho tới khi các lần sai đó cũ hơn 1 giờ
Thẻ bị khoá / hết hạnChỉ báo cho người nhập đúng PIN

Mọi lần kiểm đều được ghi lại, lần sai vẫn được đếm dù request trả lỗi.

4.3 Mã lỗi​

HTTPcodeKhi nàoFE làm gì
422validation_failedpan / pin sai định dạngBáo ở ô nhập
422card_pin_invalidSai số thẻ hoặc sai PIN"Số thẻ hoặc PIN không đúng"
409card_locked_too_many_attemptsThẻ đang khoá — details.locked_untilBáo thử lại sau giờ đó
429too_many_pin_attemptsUser đã sai 5 lần trong 1 giờ quaBáo thử lại sau
422card_has_no_pinThẻ không có PIN in kèmHướng dẫn liên hệ admin công ty
422card_not_activeThẻ bị khoá / hết hạn — details.statusHướng dẫn liên hệ admin công ty
Số thẻ và PIN

card-core không lưu số thẻ hay PIN ở đâu cả, và không ghi body của route này vào log. FE cũng phải như vậy: không lưu vào storage, không gửi lên analytics hay crash log, xoá ô nhập ngay sau khi gửi.

4.4 Trang web mở từ Zalo OA​

Trang web dùng đăng nhập OTP của Store (giống web xperise hiện tại, xem apis/otp.ts trong bestravel-fe-nuxt). OTP tới khách qua Zalo (ZNS) hoặc SMS; SĐT chưa có tài khoản thì tài khoản được tạo luôn.

  • Thêm origin của trang web vào biến CORS_ORIGINS của card-core.
  • Không đưa token lên URL.
  • Không muốn đăng nhập Store? Trang web dùng luồng Link thẻ bằng OTP SĐT: card-core tự gửi OTP tới SĐT, và tạo staff cho SĐT chưa có.

5. Kiểm tra trên staging​

Chưa có bộ API test Bruno cho các route trong trang này: chúng chỉ có trên staging sau khi bản card-core mới được deploy.


6. Chưa có​

Hạng mụcTình trạng
Cài đặt "cho phép tự link" theo từng corporateChưa có — corporate chỉ được báo (card.user_linked, via: card_pin) và tự bỏ link sau
Lưu cặp Zalo OA user id ↔ SĐT ↔ user idChưa có. Khi làm, cặp này chỉ dùng để gửi thông báo: OTP không chứng minh SĐT là số đăng ký của tài khoản Zalo đó
Tạo nhân viên (corporate staff) cho người tự linkChưa có — tự link chỉ cần user id. Luồng OTP SĐT chỉ tạo staff trong corporate_staffs của card-core
Lọc GET /cards theo người được linkChưa có
Bắt hạn link nằm trong hạn thẻChưa có — hai lớp lưu độc lập, portal tự cảnh báo (§2.3)
Hạn dùng lặp theo lịch (thứ, giờ trong ngày)Chưa có — chỉ có các khoảng rời nhau
Thêm hoặc xoá một window lẻChưa có — PATCH luôn gửi cả danh sách