Loyalty — đặc tả tích hợp

Trạng thái: POC · Ngày: 22/09/2026 · Service: mock-loyalty-api (đóng vai GalaxyJoy / SkyJoy)

Loyalty là làm giàu, không phải cổng chặn. Hệ thống loyalty sập thì hành khách vẫn vào được mạng ở mức nền — chỉ mất ưu đãi theo hạng. Đây là điểm khác cốt lõi so với HR.


1. Vì sao khác HR

Hai hệ thống, hai ngữ nghĩa lỗi hoàn toàn khác nhau. Nhầm lẫn hai cái này là lỗi nghiêm trọng.

HR Loyalty
Vai trò Cổng chặn Làm giàu
Không trả lời được Không cấp truy cập — fail closed Vẫn cấp mức nền
Ai bị ảnh hưởng khi sập Toàn bộ CBNV không vào được Không ai mất kết nối, chỉ mất ưu đãi
Timeout 3000 ms × 2 lần 2000 ms, không thử lại
Ai gọi Keycloak (trong lúc xác thực) Entitlement Service (sau khi đã xác thực)

Ngắn gọn: một hệ thống marketing gặp sự cố buổi chiều không được phép làm cả khoang máy bay mất mạng.

Vì lý do đó, loyalty.js và HrVerificationClient là hai lớp riêng biệt, không dùng chung helper — để không ai vô tình sao chép ngữ nghĩa lỗi từ bên này sang bên kia.

2. Vị trí trong luồng

Portal → Keycloak            xác thực, lấy danh tính
       → Entitlement
            → Loyalty        hội viên hạng gì?      ← chỉ với khách hàng
            → DMN            luật nói gì?
       → Session             mở mạng

Loyalty chảy vào Entitlement, không chảy vào Galaxy ID. Galaxy ID là lớp danh tính (ADR-002); quyền lợi là việc của Entitlement (ADR-005). Sơ đồ Luồng 1 hiện vẽ mũi tên ngược lại — xem docs/03-architecture/FLOW1-AUTHEN-REVIEW.md mục 2.4.

Không hỏi loyalty về CBNV. Quyền lợi của nhân viên đến từ xác minh HR; hỏi thêm loyalty chỉ thêm một điểm phụ thuộc mà không được gì.

3. API

POST /api/v1/members/lookup

POST http://localhost:3006/api/v1/members/lookup
Content-Type: application/json
X-API-Key: {LOYALTY_API_KEY}
X-Correlation-ID: corr-...

{ "email": "an.nguyen@example.test" }

Là hội viên:

{
  "found": true,
  "memberRef": "GJ-000101",
  "tier": "GOLD",
  "status": "ACTIVE",
  "wifiBenefit": true
}

Không phải hội viên — đây là câu trả lời bình thường, không phải lỗi. Phần lớn hành khách trên một chuyến bay sẽ rơi vào đây:

{ "found": false }
Mã HTTP Khi nào
UNAUTHORIZED 401 Thiếu hoặc sai X-API-Key
INVALID_REQUEST 400 Email sai định dạng, hoặc body không phải JSON
RATE_LIMITED 429 Vượt trần toàn cục
LOYALTY_SERVICE_TIMEOUT 504 (chỉ khi bơm lỗi thử nghiệm)
LOYALTY_SERVICE_UNAVAILABLE 503 (chỉ khi bơm lỗi thử nghiệm)

Tối thiểu hoá dữ liệu

Chỉ trả 5 trường. Không trả tên, số điện thoại, địa chỉ, điểm tích luỹ, lịch sử giao dịch, ngày sinh.

Cổng Wi-Fi cần một phán quyết, không cần hồ sơ khách hàng. Rò rỉ thì thiệt hại nhỏ. Có test tự động chặn việc thêm trường.

Hạng bị san phẳng khi tài khoản không hoạt động

Tài khoản SUSPENDED hoặc CLOSED vẫn giữ hạng lịch sử trong hệ thống loyalty, nhưng API này trả về tier: "NONE".

Lý do: bên tiêu thụ không thể vô tình tôn vinh một hạng mà tài khoản không còn được hưởng. Quyết định đó nằm ở nguồn dữ liệu, không đẩy xuống cho từng người gọi tự suy luận.

4. Khoá tra cứu — điểm cần quyết

POC tra theo email.

Ưu Đơn giản, không cần liên kết trước, đúng với thực tế "tìm hội viên đã có"
Nhược Email là dữ liệu cá nhân, đang chảy sang một hệ thống khác
Nhược Người dùng đổi email là mất liên kết

Phương án production nên xem xét: lưu member_ref làm attribute trên Galaxy ID — giống cách employee_ref đang dùng — rồi tra theo tham chiếu đó. Khi ấy email không phải rời khỏi Galaxy ID.

Ghi là T-LOYALTY-KEY.

5. Ảnh hưởng tới quyền lợi

Hạng hội viên là một đầu vào của bảng quyết định DMN. Xem DMN-DECISION-SPEC.vi.md.

Hạng Gói Tier Thiết bị Hiệu lực
GOLD WIFI_CUSTOMER_PLUS BOOST 2 2h
SILVER WIFI_CUSTOMER_PLUS BASIC 2 1h
NONE, không tra được WIFI_CUSTOMER_BASIC BASIC 1 1h

⚠️ Toàn bộ bảng trên là PLACEHOLDER của POC. Tên hạng và mỗi hạng được hưởng gì chưa được nghiệp vụ xác nhận. Xem ADR-011 và câu hỏi mở T1 — đòn bẩy là thời lượng hay tốc độ vẫn chưa chốt.

Sửa được ngay trong bảng DMN, không cần đụng vào mã.

sourceType của quyền lợi ghi rõ nguồn gốc:

Nguồn sourceType sourceRef
Hạng hội viên LOYALTY_TIER GJ-000101
Xác minh CBNV EMPLOYEE_VERIFICATION EMP-12345
Chỉ đăng nhập Galaxy ID GALAXY_ID_LOGIN null

Nhìn một bản ghi entitlement là biết vì sao người đó có quyền đó.

6. Hành vi khi suy giảm

loyalty.js không bao giờ ném lỗi. Mọi nhánh hỏng đều trả về "không có hạng", kèm trường source ghi lại lý do — để về sau còn giải thích được vì sao hành khách nhận mức thấp hơn.

source Nghĩa là
loyalty Tra được, có hội viên
not-a-member Tra được, không phải hội viên — bình thường
skipped Là CBNV, không hỏi loyalty
timeout Quá 2000 ms
unreachable Không kết nối được
http-503, http-401… Loyalty trả lỗi
not-configured LOYALTY_URL chưa đặt
bad-response Trả về không đọc được

Ba giá trị đầu là bình thường. Các giá trị còn lại đều ghi warn trong log — nếu không thì không ai biết vì sao một khách GOLD lại chỉ nhận BASIC.

7. Dữ liệu mock

Toàn bộ tổng hợp. .test là TLD dành riêng theo RFC 6761, vĩnh viễn không resolve được.

Cố ý KHÔNG trùng khớp với danh sách người dùng Galaxy ID. Có hội viên không có Galaxy ID, có người dùng Galaxy ID không phải hội viên. Đó là trạng thái bình thường của hai hệ thống do hai đội khác nhau sở hữu, và nền tảng phải sống được với nó.

Email memberRef Hạng Trạng thái Quyền lợi Wi-Fi Kết quả
an.nguyen@example.test GJ-000101 GOLD ACTIVE ✅ WIFI_CUSTOMER_PLUS · BOOST · 2 tb
dung.pham@example.test GJ-000104 GOLD ACTIVE ✅ như trên
linh.dao@example.test GJ-000109 GOLD ACTIVE ✅ (chưa có Galaxy ID)
phuc.ngo@example.test GJ-000112 GOLD ACTIVE ✅ (chưa có Galaxy ID)
binh.tran@example.test GJ-000102 SILVER ACTIVE ✅ WIFI_CUSTOMER_PLUS · BASIC · 2 tb
em.hoang@example.test GJ-000105 SILVER ACTIVE ✅ như trên
minh.ly@example.test GJ-000110 SILVER ACTIVE ✅ (chưa có Galaxy ID)
chi.le@example.test GJ-000103 NONE ACTIVE ✅ WIFI_CUSTOMER_BASIC · 1 tb
nga.trinh@example.test GJ-000111 NONE ACTIVE ✅ (chưa có Galaxy ID)
giang.vo@example.test GJ-000106 GOLD SUSPENDED ❌ ⚠️ san về NONE → BASIC
hoa.dang@example.test GJ-000107 GOLD ACTIVE ❌ ⚠️ GOLD nhưng không có quyền lợi Wi-Fi → BASIC
khanh.bui@example.test GJ-000108 SILVER CLOSED ❌ ⚠️ san về NONE → BASIC

Ba dòng cuối là các trường hợp đáng thử. Hạng cao nhưng không được hưởng — mỗi dòng một lý do khác nhau.

Người dùng Galaxy ID không có trong loyalty thì nhận found: false và mức nền. Đây là trường hợp phổ biến nhất trên thực tế.

8. Bơm lỗi để thử

# Loyalty sập — khách vẫn phải vào được
curl -X POST localhost:3006/admin/fault -H 'Content-Type: application/json' \
  -d '{"mode":"unavailable"}'

# Loyalty treo — đăng nhập không được treo theo
curl -X POST localhost:3006/admin/fault -H 'Content-Type: application/json' \
  -d '{"mode":"timeout","delayMs":5000}'

# Trả lại bình thường
curl -X POST localhost:3006/admin/fault -H 'Content-Type: application/json' \
  -d '{"mode":"none"}'

POST /admin/fault là test harness, không xác thực. Phải xoá trước khi deploy.

Đo thực tế: loyalty treo 5 s thì đăng nhập vẫn xong trong ~2,1 giây và trả về mức nền.

9. Câu hỏi còn mở

Ref Câu hỏi
T-LOYALTY-KEY Tra theo email hay theo member_ref lưu trên Galaxy ID?
T-TIER Tên hạng và quyền lợi mỗi hạng — chưa nghiệp vụ nào xác nhận. ADR-011, T1
T-LOYALTY-SOR Hệ thống loyalty thật là gì — GalaxyJoy, SkyJoy, hay cả hai? Hợp đồng API ra sao?
T-LOYALTY-SYNC Tra đồng bộ trong luồng đăng nhập, hay đồng bộ trước rồi cache?
T-LOYALTY-PII Gửi email sang hệ thống loyalty có cần cơ sở pháp lý riêng không?

API-OVERVIEW.md · KEYCLOAK-INTEGRATION-SPEC.vi.md · DMN-DECISION-SPEC.vi.md · DATA-CLASSIFICATION.md

GIW POC Identity Platform · bản demo local · không phải production · sinh từ repository