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ó.
| 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/faultlà 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