diff --git a/Use_Cases_Analysis.md b/Use_Cases_Analysis.md
index 3609d9f..f667306 100644
--- a/Use_Cases_Analysis.md
+++ b/Use_Cases_Analysis.md
@@ -1,7 +1,3 @@
-# Phân tích Use Cases - Event Processor Service
-
-Dựa trên các trao đổi và hệ sinh thái hiện tại của nền tảng, tài liệu này đã được cập nhật lại để làm rõ luồng xử lý và kiến trúc cho 13 Use Cases.
-
## 1. Các Thành phần Hệ thống Hiện tại (System Topology)
Dưới đây là các component đang có sẵn và vai trò của chúng trong luồng xử lý sự kiện:
- **Nguyên tắc cốt lõi:** Các giao dịch tài chính/điểm thưởng cần sự chính xác, an toàn tuyệt đối.
@@ -9,10 +5,10 @@ Dưới đây là các component đang có sẵn và vai trò của chúng trong
- **PostgreSQL:** Database chính yếu (lưu trữ an toàn, ACID).
- **Redis:** Hiện tại CHỈ làm cache server, chỉ dùng khi thực sự cần thiết (Không dùng làm primary data store cho counter/state quan trọng).
- **Doris:** Data Warehouse (Dùng cho BI, Dashboard, Reporting, Segment).
-- **transaction-service:** Core Engine của hệ thống (Xử lý giao dịch tài chính, **Rule Engine**, cộng/trừ điểm, tặng voucher, xử lý các bộ đếm - counter). Lắng nghe (consume) từ Kafka `events-topic`.
+- **transaction-service:** Core Engine của hệ thống (Xử lý giao dịch tài chính, **Rule Engine**, cộng/trừ điểm, tặng voucher, xử lý các bộ đếm Rule counter, giới hạn thưởng). Lắng nghe (consume) từ Kafka `events-topic`.
- **event-processor-service:** Đóng vai trò **Event Gateway/Router**. Nhận event từ các nguồn (Client/Service) -> Validate/Định tuyến -> Đẩy vào Kafka `events-topic`.
- **notification-service:** Chịu trách nhiệm push noti (SMS/Email/Firebase). Lắng nghe từ Kafka `noti-topic`.
-- **marketing-service:** Chịu trách nhiệm lập lịch (Scheduler), quét tập khách hàng theo chiến dịch.
+- **marketing-service:** Lập lịch (Scheduler), quét tập khách hàng theo chiến dịch.
- **attribute-service:** Quản lý Dynamic Attribute (mô hình EAV) để tạo thuộc tính động cho Customer, Card, Product_Account, Items
- **member-app & merchant-app:** Ứng dụng Frontend cho khách hàng và đối tác.
@@ -38,99 +34,6 @@ Dưới đây là các component đang có sẵn và vai trò của chúng trong
---
-## 3. Đánh giá lại Kiến trúc (Re-evaluation)
-
-Với hệ sinh thái hiện tại (đặc biệt: `transaction-service` là Rule Engine + Counter, và `Redis` chỉ làm cache), vai trò của `event-processor-service` đã thay đổi so với góc nhìn ban đầu. Nó KHÔNG phải là nơi tính toán Rule (Rule Engine), mà là **Cửa ngõ tiếp nhận và định tuyến sự kiện (Event Ingestor & Router)**.
-
-Dưới đây là đánh giá lại phương án xử lý cho từng nhóm vấn đề:
-
-### Vấn đề 1: Xử lý Login & Counter (UC-01 & UC-02)
-- **Ràng buộc:** Event `Login` có tần suất rất cao (nhiều lần/ngày/user). Nếu đẩy tất cả vào `transaction-service` sẽ gây nghẽn Core Engine. Tuy nhiên, UC-01 lại yêu cầu realtime cho lần đầu.
-- **Phương án điều chỉnh (Tách biệt logic qua Attribute Service):**
- - **UC-01 & UC-02 (First Login & Daily Login):** Đều sử dụng chung event `Login` (Loại **Counter Event**), được đẩy vào `tracking-topic`.
- - `attribute-service` sẽ hứng `Login` và thực hiện đồng thời 2 rule:
- - **First Login Bonus (UC-01):** Sử dụng instruction `SET_TRUE_ONCE` trên attribute `first_login_flag`. Nếu gán True thành công (tức là đăng nhập lần đầu), hệ thống sẽ trigger event `First_Login_Reward` gửi vào `events-topic` để TP cộng 100 điểm ngay lập tức.
- - **Daily Login Counter (UC-02):** Tính toán `last_login_date` và `current_streak`. Tránh được việc lưu rác vào TP (Idempotent nếu login nhiều lần 1 ngày). Khi streak chạm 7 hoặc 30 ngày, bắn trigger event (`Login_Streak_7_Days`) vào `events-topic`.
-
-### Vấn đề 2: Xử lý Tracking & Counter Events — Pattern "Accumulate & Trigger" (UC-03 & UC-12)
-- **Ràng buộc:** `transaction-service` xử lý giao dịch tài chính. Tuyệt đối không để các event theo dõi hành vi (tracking) làm nghẽn luồng tài chính cốt lõi.
-- **Ý kiến của anh:** Cần có giải pháp để xác định event thuộc loại **counter** (có thể cân nhắc Dynamic Attribute), phân loại vào topic riêng. Chỉ khi đạt mức cấu hình (threshold) thì mới trigger gửi vào `events-topic` để `transaction-service` xử lý.
-
-- **Phương án: Pattern "Accumulate & Trigger" dùng Dynamic Attribute**
-
- **Ý tưởng cốt lõi:** `event-processor-service` phân loại event thành 2 nhóm:
- - **Direct Events** (Login, Redeem, Transfer...): Đẩy thẳng `events-topic` → `transaction-service`.
- - **Counter Events** (ViewItem, RedeemItem count...): Đẩy vào `tracking-topic` → tích lũy counter qua `attribute-service` → chỉ khi chạm ngưỡng mới trigger `events-topic`.
-
- **Luồng xử lý chi tiết:**
- ```
- member-app ──(ViewItem)──▶ event-processor-service
- │
- │ Phân loại: Counter Event
- ▼
- tracking-topic (Kafka)
- │
- ▼
- attribute-service
- │
- ┌──────────┴──────────┐
- │ 1. Tăng counter │
- │ (Dynamic Attr) │
- │ 2. Check threshold │
- │ (cấu hình) │
- └──────────┬──────────┘
- │
- Counter < threshold?
- ┌───────┴───────┐
- │ YES │ NO (đạt ngưỡng)
- ▼ ▼
- (dừng) events-topic (Kafka)
- │
- ▼
- transaction-service
- (cộng điểm/voucher)
- ```
-
- **Ví dụ cụ thể cho UC-03 (View Item):**
- - Cấu hình Rule: "Xem 10 sản phẩm khác nhau trong tuần → thưởng 50 điểm".
- - `attribute-service` tạo Dynamic Attribute `weekly_view_count` (type: INTEGER) cho entity `Customer`.
- - Mỗi event `ViewItem` → `attribute-service` tăng counter `weekly_view_count` += 1.
- - Khi `weekly_view_count` >= 10 → `attribute-service` bắn event `Counter_Threshold_Reached` vào `events-topic`.
- - `transaction-service` hứng event → cộng 50 điểm → reset counter.
-
- **Ví dụ cụ thể cho UC-12 (Most Redeem Item):**
- - Cấu hình Rule: Counter thuần túy, không cần trigger (chỉ cập nhật số lượng).
- - `attribute-service` tạo Dynamic Attribute `total_redeem_count` (type: INTEGER) cho entity `Product` (Item).
- - Mỗi event `ItemRedeemed` → `attribute-service` tăng counter → Portal query attribute này để hiển thị "Most Popular".
-
- **Thống nhất giải pháp cho 2 câu hỏi mở (Theo hướng giảm tải TP):**
-
- **Q1: Ai quản lý cấu hình threshold?**
- - **Phương án chốt:** Đặt cấu hình threshold ngay tại **cấu hình events / metadata của Dynamic Attribute** (trong `attribute-service`), tách biệt hoàn toàn khỏi `transaction-service`.
- - **Lý do & Luồng chạy:** Để giảm tải tối đa cho `transaction-service` (TP). Nhóm Event/Attribute sẽ hoạt động độc lập (self-contained).
- - Attribute `weekly_view_count` sẽ lưu kèm cấu hình: `{ threshold: 10, trigger_event: "View_10_Items_Reward" }`.
- - Cứ mỗi event `ViewItem`, `attribute-service` tự tăng counter và tự đối chiếu với threshold nội tại.
- - CHỈ KHI chạm ngưỡng 10, `attribute-service` mới sinh ra 1 event `View_10_Items_Reward` đẩy vào `events-topic`. Lúc này TP mới nhảy vào cuộc để cộng điểm.
-
- **Q2: Reset counter khi nào? Ai chịu trách nhiệm?**
- - **Phương án chốt:** Hoạt động theo mô hình tự trị.
- - **Tự reset khi Trigger:** Ngay khoảnh khắc `attribute-service` đẩy event `View_10_Items_Reward` đi, nó tự động reset counter của user đó về 0 (hoặc trừ đi 10). Không cần đợi TP xử lý xong để giảm giao tiếp vòng vèo (vì đây là event tracking, chấp nhận eventual consistency).
- - **Reset theo chu kỳ (Time-based):** Đối với các counter có thời hạn (ví dụ: đếm trong tuần), hệ thống cần cơ chế Cronjob (từ `marketing-service` hoặc nội tại `attribute-service`) quét và reset toàn bộ counter về 0 vào lúc 00:00 thứ Hai hàng tuần.
-
-### Vấn đề 3: Dynamic Attribute cho Khảo sát (UC-08)
-- **Phương án điều chỉnh (Event-Driven hoàn toàn):**
- - Trạng thái "đã làm khảo sát" được lưu động bằng EAV trong `attribute-service`.
- - Không cần gọi API đồng bộ. Khi user hoàn thành, `member-app` chỉ cần bắn event `Survey_Completed` (loại `COUNTER`) về `event-processor-service`.
- - Event Processor sẽ tự động route song song:
- 1. Đẩy vào `events-topic`: `transaction-service` hứng để tặng voucher.
- 2. Đẩy vào `tracking-topic`: `attribute-service` hứng và thực thi lệnh `SET_TRUE_ONCE` để update attribute `survey_completed = true`.
-
-### Vấn đề 4: Time-based Triggers & Cross-System (UC-11, UC-13, UC-10)
-- **UC-11 & UC-13 (Retention):** `marketing-service` chạy Cronjob quét database (PostgreSQL). Tìm ra danh sách User thoả mãn -> Bắn event vào `events-topic`. `transaction-service` hứng và cấp ưu đãi, sau đó đẩy thông báo sang `noti-topic`.
-- **UC-10 (Merchant Scan Voucher):** `merchant-app` quét mã -> gọi API của `transaction-service`. `transaction-service` xử lý trừ voucher trong DB (đảm bảo ACID). Thành công -> bắn event `Voucher_Redeemed` vào `noti-topic` -> `notification-service` hứng và gọi Firebase push realtime hiển thị thông báo lên `member-app`.
-
----
-
## 4. Tóm lược Vai trò của Event Processor
Dưới góc nhìn kiến trúc mới nhất, `event-processor-service` là một **Smart Event Gateway**:
@@ -138,11 +41,11 @@ Dưới góc nhìn kiến trúc mới nhất, `event-processor-service` là mộ
1. **Ingest & Validate:** Hứng mọi loại event từ Client (member-app, hệ thống ngoài). Đảm bảo schema của event đúng định dạng chuẩn.
2. **Classify (Phân loại):** Xác định event thuộc loại nào:
- **Direct Event** → Cần xử lý logic/tài chính ngay.
- - **Counter Event** → Cần tích lũy trước khi trigger.
+ - **Tracking Event** → Cần tích lũy trước khi trigger.
- **Notification Event** → Cần gửi thông báo trực tiếp.
3. **Routing (Định tuyến):**
- Direct Events (Redeem, Transfer, Profile_Completed...) → `events-topic` → `transaction-service`.
- - Counter Events (Login, ViewItem, RedeemItem count...) → `tracking-topic` → `attribute-service` (tích lũy, khi đạt threshold → trigger `events-topic`).
+ - Tracking Events (Login, ViewItem, RedeemItem count...) → `tracking-topic` → `attribute-service` (tích lũy, khi đạt threshold → trigger `events-topic`).
- Notification Events → `noti-topic` → `notification-service`.
```
@@ -180,62 +83,6 @@ Dưới góc nhìn kiến trúc mới nhất, `event-processor-service` là mộ
---
-## 5. Các Câu hỏi Cần Bàn bạc với Team
-
-> Các phương án ở trên dựa trên giả định ban đầu trong quá trình phân tích. Dưới đây là tổng hợp tất cả các quyết định kiến trúc cần team xác nhận hoặc thảo luận thêm.
-
-### 5.1. Kiến trúc Tổng quan
-
-| # | Câu hỏi | Giả định hiện tại | Cần xác nhận |
-|---|---------|-------------------|-------------|
-| A1 | **Fat Event vs Thin Event:** Event gửi vào hệ thống nên chứa sẵn context (Fat) hay chỉ chứa ID (Thin) rồi service tự tra cứu? | **Hybrid:** Profile API bắn Fat Event (`Profile_Completed`), còn Login là Thin Event (Event Processor/TP tự check). | Team có đồng ý mô hình hybrid này không? Có event nào khác cần là Fat Event? |
-| A2 | **Vai trò của `event-processor-service`:** Nó chỉ là Gateway/Router hay cần tham gia xử lý logic? | Chỉ làm **Smart Gateway**: Ingest → Classify → Route. Không chứa business logic. | Có trường hợp nào Event Processor cần tự xử lý logic trước khi route không? |
-| A3 | **Phân tách Kafka Topics:** Hiện tại có `events-topic` và `noti-topic`. Có cần thêm `tracking-topic` cho counter events không? | **Cần thêm** `tracking-topic` để tách luồng tracking/counter khỏi luồng giao dịch tài chính. | Team đồng ý tách thêm topic? Hay dùng cách khác (VD: header-based routing trên cùng 1 topic)? |
-
-### 5.2. Xử lý Login & Trạng thái Khách hàng (UC-01, UC-02, UC-05)
-
-| # | Câu hỏi | Giả định hiện tại | Cần xác nhận |
-|---|---------|-------------------|-------------|
-| B1 | **First Login Event (UC-01):** Dùng `Login` chung với chuỗi đăng nhập. | **Dùng `SET_TRUE_ONCE` để bắn trigger event `First_Login_Reward`.** | Cần đảm bảo UI Client xử lý popup thưởng 100 điểm realtime khi nhận Noti trả về. |
-| B2 | **Phát hiện "Inactive" (UC-05) như thế nào?** Bao nhiêu ngày không đăng nhập? | **Sử dụng phép toán `CHECK_AND_UPDATE_TIMER` tại Event Processor.** Khi Login, EP đóng gói lệnh check `last_login_date` gửi sang Attribute Service tính toán khoảng thời gian (Gap). | Chốt lại ngưỡng bao nhiêu ngày thì gửi Noti Welcome Back (VD: 90 ngày)? Có cần cấu hình ngưỡng này khác nhau cho từng hạng thẻ không? |
-| B3 | **Reset Counter Login (UC-02):** Khi user lỡ 1 ngày, streak bị reset về 0. | **`attribute-service` tự so sánh `last_login_date` để reset streak.** | Logic "ngày" tính theo múi giờ nào (VD: UTC+7)? Hệ thống có ghi nhận time zone của user không? |
-| B4 | **Giới hạn 1k điểm/năm (UC-02):** Tính theo năm dương lịch hay 365 ngày kể từ lần thưởng đầu tiên? | **Năm dương lịch** (01/01 - 31/12). | Xác nhận lại logic tính năm. |
-
-### 5.3. Counter Events & Tracking (UC-03, UC-12)
-
-| # | Câu hỏi | Giả định hiện tại | Cần xác nhận |
-|---|---------|-------------------|-------------|
-| C1 | **Pattern "Accumulate & Trigger":** Counter events (ViewItem) được tích lũy ở `attribute-service` bằng Dynamic Attribute. Khi đạt threshold thì mới trigger sang `events-topic`. | **`attribute-service` đảm nhận việc đếm và check threshold.** | `attribute-service` hiện có khả năng consume Kafka topic không? Hay cần build thêm consumer module? |
-| C2 | **Cấu hình threshold nằm ở đâu?** | **Đề xuất: Gắn vào metadata của Dynamic Attribute** (VD: attribute `weekly_view_count` có field `threshold: 10`). Attribute-service self-contained. | Hay nên để `transaction-service` (Rule Engine) quản lý threshold vì đó là business rule? |
-| C3 | **Reset counter khi nào?** | **Kết hợp:** (1) Reset sau khi thưởng (TP bắn `Counter_Reset_Command`), (2) Reset theo chu kỳ bởi `marketing-service` Cronjob (dọn dẹp). | Chu kỳ reset cụ thể là gì (weekly/monthly)? Có counter nào không cần reset (UC-12: tổng lượt redeem)? |
-| C4 | **UC-03: Dữ liệu "Recently Viewed" (Recommend for You):** Lưu ở đâu để query nhanh cho trang chủ? | **Dynamic Attribute trong PostgreSQL**, có thể dùng Redis làm cache. | Có yêu cầu về thời gian phản hồi (latency) cho phần Recommend? Có cần real-time hay batch update là đủ? |
-
-### 5.4. Luồng Giao dịch & Orchestration (UC-04, UC-06, UC-09, UC-10)
-
-| # | Câu hỏi | Giả định hiện tại | Cần xác nhận |
-|---|---------|-------------------|-------------|
-| D1 | **UC-09 (Redeem):** Event Processor chỉ route, `transaction-service` hoàn tất giao dịch (trừ điểm, chốt kho), `notification-service` gửi noti. | **Event Processor KHÔNG tham gia xử lý giao dịch.** | Luồng Redeem hiện tại đã hoạt động theo mô hình này chưa, hay đang đi thẳng API? |
-| D2 | **UC-10 (Merchant Scan Voucher):** `merchant-app` gọi thẳng API `transaction-service`, không đi qua Event Processor. | **Luồng đồng bộ (sync)** vì cần response ngay cho Merchant. | Merchant app gọi API nào hiện tại? Có cần bổ sung API mới không? |
-| D3 | **UC-04 (Transfer Balance):** Chuyển điểm giữa 2 tài khoản — giao dịch kép (debit + credit). | **`transaction-service` xử lý cả 2 vế trong 1 transaction ACID.** | Đã có API transfer chưa? Có giới hạn số điểm chuyển không? |
-
-### 5.5. Scheduled Jobs & Retention (UC-07, UC-08, UC-11, UC-13)
-
-| # | Câu hỏi | Giả định hiện tại | Cần xác nhận |
-|---|---------|-------------------|-------------|
-| E1 | **UC-07 (Update Profile):** Logic "đầy đủ thông tin" do ai check? | **Profile API tự check rồi emit `Profile_Completed_Event`.** | Profile API hiện có logic check completeness chưa? Danh sách field bắt buộc gồm những gì? |
-| E2 | **UC-07 (Update Profile):** "Trước ngày 30/09" — đây là ngày cố định hay cấu hình được? | **Cố định theo campaign.** | Nếu campaign thay đổi mốc thời gian thì cập nhật ở đâu? |
-| E3 | **UC-08 (Survey):** Trạng thái "đã làm khảo sát" lưu bằng Dynamic Attribute trong `attribute-service`. | **Tạo attribute `survey_completed` (BOOLEAN) cho entity Customer.** | Nếu có nhiều survey khác nhau thì sao? Cần 1 attribute per survey hay 1 attribute chung? |
-| E4 | **UC-11 (Password Expiring):** `marketing-service` quét tập khách hàng có mật khẩu sắp hết hạn. | **`marketing-service` chạy Cronjob.** | Thông tin "ngày hết hạn mật khẩu" nằm ở DB nào? `marketing-service` có quyền truy cập DB đó không? Hay cần gọi API từ IAM? |
-| E5 | **UC-13 (Dormant 3 tháng):** Xác định txn cuối cùng, không tính txn auto (phát sinh bởi OLS). | **`marketing-service` query bảng transaction, filter loại trừ txn auto.** | Txn auto có flag riêng để phân biệt không? Query có nặng không nếu bảng transaction lớn? |
-
-### 5.6. Notification & Push Realtime (UC-10)
-
-| # | Câu hỏi | Giả định hiện tại | Cần xác nhận |
-|---|---------|-------------------|-------------|
-| F1 | **Push realtime lên Portal:** Khi Merchant quét voucher, Portal khách hàng nhận thông báo ngay lập tức. | **Dùng Firebase Cloud Messaging (đã có sẵn).** | Firebase hiện push tới `member-app` (mobile) hay cả web portal? Cần kiểm tra scope tích hợp. |
-
----
-
## 6. Sơ đồ Kiến trúc Tổng thể & Giải pháp Chi tiết từng Use Case
### 6.1. Architecture Diagram — Tổng thể hệ thống
@@ -275,7 +122,7 @@ graph TB
%% Event Processor → Kafka
EP -->|"Direct Events"| ET
- EP -->|"Counter Events"| TT
+ EP -->|"Tracking Events"| TT
EP -->|"Notification Events"| NT
%% Kafka → Consumers
@@ -360,7 +207,7 @@ sequenceDiagram
participant NS as notification-service
MA->>EP: Login Event
- EP->>EP: Classify: Counter Event
+ EP->>EP: Classify: Tracking Event
EP->>TT: Publish Login
TT->>ATTR: Consume Login Event
@@ -420,7 +267,7 @@ sequenceDiagram
participant TP as transaction-service
MA->>EP: Login Event
- EP->>EP: Classify: Counter Event
+ EP->>EP: Classify: Tracking Event
EP->>TT: Publish Login
TT->>ATTR: Consume Login Event
@@ -465,7 +312,7 @@ sequenceDiagram
participant REDIS as Redis (Cache)
MA->>EP: ViewItem Event
{itemId, customerId}
- EP->>EP: Classify: Counter Event
+ EP->>EP: Classify: Tracking Event
EP->>KT: Publish to tracking-topic
KT->>ATTR: Consume ViewItem
@@ -485,7 +332,7 @@ sequenceDiagram
```
**Giải pháp:**
-- **Classification:** `event-processor-service` phân loại `ViewItem` là Counter Event → đẩy `tracking-topic`.
+- **Classification:** `event-processor-service` phân loại `ViewItem` là Tracking Event → đẩy `tracking-topic`.
- **attribute-service** xử lý 3 việc:
1. Tăng `item_view_count` (Dynamic Attribute trên entity **Item**) — phục vụ ranking/trending.
2. Cập nhật `recently_viewed` (Dynamic Attribute trên entity **Customer**) — danh sách item gần đây. Tính năng này sử dụng phép toán `APPEND_UNIQUE` lấy từ `event.itemId`, kết hợp cơ chế FIFO với giới hạn `maxLength` (VD: 50 items) để chống phình to dữ liệu.
@@ -1020,48 +867,4 @@ Khi đạt Threshold, Attribute Service đóng gói một Event hợp lệ (Dire
**Transaction Service (TP)**:
Lắng nghe `events-topic`, bắt được event `Login_Streak_7_Days`, lục tìm Rule Engine và thấy quy định: *"Tặng 100 điểm"*. Tiến hành trừ kho và cộng điểm, kết thúc flow.
----
-
-## 8. Phân tích Edge Cases & Giải pháp Kỹ thuật (Mitigations)
-
-Để đảm bảo hệ thống an toàn ở quy mô lớn, chúng ta cần xử lý triệt để các Edge Cases tiềm ẩn (đặc biệt là các lỗi liên quan đến đồng thời - Concurrency và Idempotency). Dưới đây là 2 Edge Cases nguy hiểm nhất và giải pháp thiết kế tương ứng.
-
-### 8.1. Vấn đề "Threshold Jumping" (Cộng dồn vượt ngưỡng)
-**Ngữ cảnh:**
-Thuộc tính `weekly_view_count` đang có giá trị là 9. Khách hàng sử dụng tool gửi liên tục 5 request `ViewItem` vào cùng 1 tích tắc. Các Kafka consumer xử lý 5 event này gần như đồng thời.
-
-**Rủi ro:**
-Nếu không kiểm soát đồng thời (Concurrency Control), giá trị có thể được tăng lên 14. Nếu logic trigger chỉ đơn thuần là `if (new_value >= threshold)` (14 >= 10), hệ thống sẽ lọt vào nhánh kích hoạt (Trigger) tới 5 lần, dẫn đến việc tặng điểm 5 lần cho 1 mốc.
-
-**Giải pháp (Boundary Check & DB Row Lock):**
-1. **Pessimistic Locking (Khóa bi quan):** Tại `attribute-service`, mọi hành động cập nhật `customer_attribute_values` đều phải sử dụng khóa dòng (Row-level lock) trong PostgreSQL (`SELECT ... FOR UPDATE`). Điều này buộc 5 request đồng thời phải xếp hàng chạy tuần tự.
-2. **Boundary Condition (Điều kiện ranh giới):** Logic trigger phải được cập nhật thành:
- `if (old_value < threshold AND new_value >= threshold) THEN trigger()`
-3. **Luồng thực thi an toàn:**
- - **Req 1:** old=9, new=10 -> TRÚNG ĐIỀU KIỆN (9 < 10 và 10 >= 10) -> Bắn Trigger -> Reset value về 0.
- - **Req 2:** old=0, new=1 -> TRƯỢT.
- - Các Req 3, 4, 5 tương tự sẽ làm value tăng lên 2, 3, 4 mà không bao giờ kích hoạt lại Threshold.
-
-**Rủi ro DB Contention (Đặc biệt lưu ý):**
-Dù Kafka đã đứng làm buffer (không làm sập upstream), nhưng nếu có hàng ngàn event `ViewItem` cho **cùng một sản phẩm HOT** đẩy vào, các Kafka Consumer Threads sẽ cùng lúc thực thi `SELECT ... FOR UPDATE` lên đúng 1 dòng (row) của entity `ITEM` trong DB. Điều này gây ra **Database Contention (Tắc nghẽn DB)** giữa các thread xử lý, giới hạn thông lượng (throughput) và có thể gây lock timeout hoặc thread starvation.
-
-*Cách xử lý:* Cần phân loại Counter:
-1. **Entity-Level Counter (Customer):** VD `weekly_view_count`, `login_streak`. Mức độ đồng thời trên 1 khách hàng là cực kỳ thấp -> **Dùng Row-level Lock (Pessimistic Locking)** là hoàn toàn phù hợp và an toàn tuyệt đối.
-2. **Global/Hot Item Counter:** VD `item_view_count`, `total_redeem_count`. Mức độ đồng thời trên 1 row cực kỳ cao. -> **Không nên lock trực tiếp vào DB**. Hãy chuyển các counter thuần tracking này lên **Redis (Redis INCR)** để đếm trong RAM, sau đó cấu hình 1 Scheduler (VD: chạy mỗi 10 giây) để Flush (Batch Update) giá trị từ Redis xuống PostgreSQL.
-
-### 8.2. "Bóng ma" HTTP Timeout tại Merchant App (UC-10)
-**Ngữ cảnh:**
-Merchant quét Voucher, App gọi Sync API POST lên `transaction-service`.
-TP xử lý trừ Voucher thành công (đã commit DB), nhưng quá trình trả Response (HTTP 200 OK) về cho App bị gián đoạn do rớt mạng (Network Timeout).
-
-**Rủi ro:**
-Merchant App nhận lỗi Timeout, Merchant tưởng chưa thành công nên bấm quét lại. TP nhận request thứ 2, thấy Voucher đã ở trạng thái USED nên báo lỗi "Voucher đã được sử dụng". Merchant không chịu giao hàng cho khách.
-
-**Giải pháp (Idempotency Key):**
-Thiết kế Idempotent API cho mọi endpoint đồng bộ (Sync API).
-1. **Client Generation:** Merchant App tự sinh ra một UUID (VD: `Idempotency-Key: req-556677`) và đính kèm vào Header của Request đầu tiên.
-2. **TP Caching / DB Table:** `transaction-service` lưu một bảng `idempotent_requests(key, status, response_body)`.
- - Lần gọi 1: TP tra bảng thấy key chưa tồn tại -> Xử lý giao dịch -> Commit -> Lưu `status=SUCCESS`, `response_body={"msg": "Thành công"}` vào bảng. Do rớt mạng, App không nhận được.
- - Lần gọi 2 (Retry): App gửi nguyên lại `Idempotency-Key: req-556677`.
- - TP tra bảng, thấy key đã tồn tại và `status=SUCCESS`. Nó **bỏ qua giao dịch** và chỉ trả về nguyên vẹn cái `response_body` cũ (HTTP 200 OK).
-3. **Kết quả:** Merchant App nhận được phản hồi thành công như chưa từng có lỗi mạng xảy ra. Không hề có lỗi "Voucher đã sử dụng".
\ No newline at end of file
+---
\ No newline at end of file
diff --git a/specs/Event_Processor_HLD.md b/specs/Event_Processor_HLD.md
new file mode 100644
index 0000000..e450118
--- /dev/null
+++ b/specs/Event_Processor_HLD.md
@@ -0,0 +1,626 @@
+# Event Processor Service — High-Level Design
+## Hệ thống Event-Driven Loyalty & Rewards
+
+---
+
+### 1. Tổng quan (System Overview)
+
+#### 1.1. Mục tiêu
+
+- **Chuẩn hóa xử lý Event:** Tạo ra một flow chung duy nhất cho mọi luồng sự kiện từ các Client/App gửi về hệ thống.
+- **Giảm tải Transaction Service (TP):** Bảo vệ core engine (TP) khỏi hàng triệu sự kiện "rác" (như Login, View, Click). TP chỉ thức dậy khi có giao dịch tài chính hoặc cần phát thưởng thực sự.
+- **Cho phép Marketing cấu hình Campaign linh hoạt:** Cung cấp bộ công cụ (thông qua Attribute/Event Processor) để cấu hình các ngưỡng thưởng (threshold) đa dạng mà không cần Dev phải sửa code.
+- **Đảm bảo giao dịch tài chính ACID:** Các giao dịch cộng/trừ điểm, phát voucher, trừ kho vẫn được xử lý tập trung tại Transaction Service với độ an toàn dữ liệu cao nhất.
+
+#### 1.2. Nguyên tắc thiết kế
+
+1. **Transaction Service** CHỈ xử lý các nghiệp vụ tài chính, Rule Engine (cộng/trừ điểm, tặng voucher, giới hạn thưởng). Không giữ các bộ đếm rác.
+2. **Attribute Service** (Dumb Executor) CHỈ làm nhiệm vụ lưu trữ trạng thái (state), tính toán các phép toán cơ bản (cộng dồn, đếm ngày, kiểm tra khoảng thời gian) dựa trên chỉ thị. Hoàn toàn KHÔNG biết về business logic (như thưởng bao nhiêu, rule gì).
+3. **Event Processor** (Smart Gateway) CHỈ làm nhiệm vụ Validate (kiểm tra định dạng) và Routing (phân loại & điều phối luồng sự kiện). Đóng vai trò là nơi lưu trữ cấu hình (Event Registry).
+4. **Marketing Service** CHỈ làm nhiệm vụ lập lịch (Scheduler) hoặc định kỳ quét tập khách hàng để phát sinh Event đầu vào. KHÔNG trực tiếp thực hiện cộng điểm hay xử lý tài chính.
+
+#### 1.3. Đối tượng & Hệ thống liên quan
+
+- **Actors (Người dùng):** Khách hàng (Member) sử dụng ứng dụng di động/web; Nhân viên đối tác (Merchant) sử dụng ứng dụng quét mã.
+- **External Systems (Hệ thống ngoài):** Hệ thống phát hành thẻ (Card Issuance), Hệ thống gửi tin (SMS/Email/Firebase).
+
+---
+
+### 2. Kiến trúc & Topology
+
+#### 2.1. Sơ đồ kiến trúc tổng quan
+
+Hệ thống được chia thành các phân lớp (layers) rõ ràng nhằm đảm bảo tính độc lập, dễ bảo trì và dễ dàng mở rộng.
+
+```mermaid
+graph TD
+ subgraph Client Layer
+ MA[Member App]
+ MRA[Merchant App]
+ end
+
+ subgraph Event Gateway Layer
+ EP[event-processor-service
Smart Router]
+ end
+
+ subgraph Message Broker Layer
+ K_ET[Kafka: events-topic]
+ K_TT[Kafka: tracking-topic]
+ K_NT[Kafka: noti-topic]
+ end
+
+ subgraph Core Service Layer
+ TP[transaction-service
Core Rule & Ledger]
+ ATTR[attribute-service
Tracking & Counter]
+ NS[notification-service
Push/SMS]
+ MKT[marketing-service
Scheduler]
+ end
+
+ subgraph Data & Caching Layer
+ DB_TP[(PostgreSQL
Transaction)]
+ DB_ATTR[(PostgreSQL
EAV/Attribute)]
+ REDIS[(Redis
Cache)]
+ end
+
+ MA -->|Events: Login, View, Redeem| EP
+ MRA -.->|Sync API: Scan Voucher| TP
+
+ EP -->|Direct Events| K_ET
+ EP -->|Tracking Events| K_TT
+ EP -->|Noti Events| K_NT
+
+ K_ET --> TP
+ K_TT --> ATTR
+ K_NT --> NS
+
+ MKT -->|Cron: Scheduled Events| EP
+ ATTR -->|Threshold Reached Trigger| K_ET
+ TP -->|Reward Result| K_NT
+
+ TP --- DB_TP
+ ATTR --- DB_ATTR
+ ATTR -.-> REDIS
+```
+
+#### 2.2. Phân loại Event
+
+Mọi sự kiện đầu vào được chia làm 3 loại duy nhất:
+
+- **Direct Event:** Cần xử lý logic/tài chính ngay lập tức (VD: Redeem Item, Transfer Balance, Welcome Offer). Sẽ được đẩy thẳng cho Transaction Service.
+- **Tracking Event:** Cần tích lũy, đếm hoặc kiểm tra điều kiện trước khi trigger (VD: Daily Login, View Item). Sẽ được đẩy cho Attribute Service để lưu trữ và tính toán.
+- **Notification Event:** Các sự kiện chỉ mang mục đích gửi thông báo, không có logic tài chính hay đếm (VD: Cảnh báo mật khẩu sắp hết hạn). Sẽ được đẩy thẳng cho Notification Service.
+
+#### 2.3. Vai trò các Service
+
+- **Event Processor:** `Validate + Route`. Nơi nhận Thin Event, tra cứu cấu hình, biến thành Fat Payload và định tuyến vào đúng Topic Kafka.
+- **Attribute:** `Tracking / State`. Cỗ máy tính toán cơ bắp. Nhận Fat Payload, thực thi các phép toán (+, -, gán cờ) vào DB. Sinh sự kiện mới nếu đạt threshold.
+- **Transaction:** `Rule Engine + Reward`. Đảm bảo tính ACID của giao dịch. Cộng điểm, trừ điểm, trả voucher theo đúng Rule được định cấu hình.
+- **Notification:** `Push`. Gửi SMS, Email, Firebase Push tới End-User.
+- **Marketing:** `Scheduler`. Quét tập dữ liệu định kỳ (cronjob) để sinh ra sự kiện tự động (như User ngủ đông, sắp sinh nhật, mật khẩu hết hạn).
+
+#### 2.4. Technology Stack
+
+| Thành phần | Công nghệ | Lý do |
+|:---|:---|:---|
+| **Backend** | Java / Spring Boot | Hệ sinh thái phong phú, độ ổn định cực cao (Enterprise-grade) chuyên trị các hệ thống tài chính/giao dịch khắt khe |
+| **Message Broker** | Apache Kafka | Khả năng xử lý thông lượng khổng lồ (high-throughput), độ trễ cực thấp, persistence và Consumer Groups |
+| **Database** | PostgreSQL | Hỗ trợ ACID mạnh mẽ cho Transaction Service, đồng thời hỗ trợ JSONB cho mô hình EAV của Attribute Service |
+| **Caching** | Redis | Tốc độ Read/Write in-memory cực nhanh, lưu trữ cache phi tài chính (recently viewed items) |
+| **Data Warehouse** | Doris | Phục vụ báo cáo, phân tích (BI) và phân tập khách hàng (Segmentation) mà không ảnh hưởng OLTP |
+
+---
+
+### 3. Luồng xử lý (Workflows)
+
+#### 3.1. Tracking Flow — "Accumulate & Trigger" (Ví dụ: View Item)
+
+**Mô tả:** Nhằm tránh làm nghẽn Core Engine bởi các sự kiện có tần suất cao, hệ thống sử dụng Attribute Service để đếm (Counter). Chỉ khi đạt ngưỡng quy định (Threshold), một sự kiện trả thưởng mới được sinh ra và đẩy cho Core Engine xử lý.
+
+```mermaid
+sequenceDiagram
+ participant MA as Member App
+ participant EP as Event Processor
+ participant K_TT as Kafka (tracking-topic)
+ participant ATTR as Attribute Service
+ participant K_ET as Kafka (events-topic)
+ participant TP as Transaction Service
+
+ MA->>EP: Gửi Event (VD: ViewItem)
+ EP->>EP: Phân loại: Tracking Event
+ EP->>K_TT: Đẩy vào tracking-topic
+ K_TT->>ATTR: Consume Event
+
+ ATTR->>ATTR: 1. Cập nhật Counter (weekly_view_count++)
2. Cập nhật Cache (recently_viewed)
+
+ alt Counter >= Threshold (VD: 10)
+ ATTR->>K_ET: Bắn Event Trả Thưởng (View_10_Items_Reward)
+ ATTR->>ATTR: Reset Counter
+ K_ET->>TP: Consume Reward Event
+ TP->>TP: Cộng điểm (Transaction ACID)
+ end
+```
+
+#### 3.2. Direct Flow — Giao dịch Tài chính Trực tiếp (Ví dụ: Redeem Item / Transfer)
+
+**Mô tả:** Các giao dịch mang tính tài chính trực tiếp (chuyển điểm, đổi quà) được gọi trực tiếp qua API tới Transaction Service để xử lý đồng bộ (Synchronous), giúp đảm bảo tốc độ phản hồi ngay lập tức cho người dùng và tính toàn vẹn dữ liệu.
+
+```mermaid
+sequenceDiagram
+ participant MA as Member App
+ participant TP as Transaction Service
+ participant K_NT as Kafka (noti-topic)
+ participant NS as Notification Service
+
+ MA->>TP: Gọi API trực tiếp (Redeem_Item / Transfer)
+
+ TP->>TP: BEGIN DB TRANSACTION
+ TP->>TP: Validate Số dư & Điều kiện
+ TP->>TP: Debit (-) Điểm & Cập nhật State/Inventory
+ TP->>TP: COMMIT TRANSACTION
+
+ TP-->>MA: Trả kết quả thành công (200 OK)
+
+ TP->>K_NT: Bắn Event Giao dịch Thành công
+ K_NT->>NS: Consume Event
+ NS->>MA: Đẩy Noti (Firebase Push/SMS)
+```
+
+#### 3.3. Payload Transformation
+
+Sự biến đổi của dữ liệu từ Client cho tới lúc phát thưởng:
+
+- **Thin Event (Từ Client):** Rất mỏng, chỉ chứa `{ event: "Login", customerId: "CUST-001", eventTime: "2026-07-16T10:30:00+07:00" }`. Không chứa rule hay logic.
+- **Fat Payload (Từ Event Processor gửi Kafka):** Chứa Thin Event + Kẹp thêm mảng các `Instructions` (lấy từ Registry). Để chỉ đạo Attribute Service phải tính toán cái gì.
+- **Trigger Event (Do Attribute publish vào events-topic):** Khi đạt Threshold, sinh ra một sự kiện rõ ràng. VD: `{ event: "Login_Streak_7_Days", customerId: "CUST-001" }`. Transaction Service chỉ cần đọc tên event này từ Kafka để trả thưởng.
+
+---
+
+### 4. Use Case Mapping
+
+#### 4.1. Use Case Template
+
+Để thống nhất ngôn ngữ giao tiếp, mọi Use Case mới sẽ được mô tả theo Template chuẩn sau:
+
+- **Business Requirement:** Yêu cầu nghiệp vụ (Mô tả ngắn gọn mục đích).
+- **Event:** Tên sự kiện gốc từ Client gửi lên (VD: `Login`, `ViewItem`).
+- **Event Type:** Phân loại (`Direct`, `Tracking`, `Notification`).
+- **Processing Flow:** Luồng xử lý (`Direct Flow` hoặc `Tracking Flow`).
+- **Attributes:** Các thuộc tính cần cập nhật lưu tại Attribute Service (VD: `login_streak`, `weekly_view_count`). Nếu là Direct Flow thì để trống.
+- **Trigger:** Tên sự kiện sinh ra khi đủ điều kiện để báo cho Transaction Service (VD: `Login_Streak_7_Days`).
+- **Output:** Kết quả cuối cùng (VD: +100 điểm, tặng 1 voucher).
+
+#### 4.2. Bảng Ánh xạ Nghiệp vụ - Kỹ thuật (UC-to-Solution Mapping)
+
+| Mã UC / Tên Use Case | Giải pháp Kỹ thuật tương ứng | Thành phần liên quan (Components) |
+| :--- | :--- | :--- |
+| **UC-01** - First Login
*(Đăng nhập lần đầu)* | Gán cờ `first_login_flag` bằng lệnh `SET_TRUE_ONCE`. Thành công sẽ kích hoạt event `First_Login_Reward` để hệ thống cộng điểm và đẩy thông báo realtime. | `attribute-service`, `transaction-service`, Kafka (`tracking-topic`, `events-topic`), PostgreSQL (Attr DB) |
+| **UC-02** - Daily Login Tracking
*(Điểm danh hàng ngày)* | Cập nhật bộ đếm `current_streak` và `last_login_date`. Bỏ qua các sự kiện trùng lặp trong ngày (Idempotency). Khi đủ 7/30 ngày, trigger event trả thưởng. | `attribute-service`, `transaction-service`, Kafka (`tracking-topic`, `events-topic`) |
+| **UC-03** - View Item
*(Xem chi tiết quà)* | Cập nhật `item_view_count`, lưu lịch sử `recently_viewed` vào Cache. Cộng `weekly_view_count`, đủ 10 views sẽ trigger event cộng điểm. | `attribute-service`, Redis (Cache), Kafka (`tracking-topic`), PostgreSQL (Attr DB) |
+| **UC-04** - Transfer Balance
*(Chuyển điểm)* | Xử lý giao dịch trừ tiền người gửi, cộng tiền người nhận trong cùng 1 ACID Transaction. Bắn push notification cho cả hai bên. | `transaction-service`, `notification-service`, Kafka (`events-topic`), PostgreSQL (Txn DB). Đã có luồng xử lý, không cần đi qua events |
+| **UC-05** - Inactive Account Login
*(Login sau thời gian ngủ đông)*| Kèm cấu hình `CHECK_AND_UPDATE_TIMER` vào event Login. Nếu GAP >= 90 ngày, trigger event `Account_Reactivated` để đổi trạng thái và gửi noti Welcome Back. | `attribute-service`, `transaction-service`, Kafka (`tracking-topic`, `events-topic`) |
+| **UC-06** - Welcome Offer
*(Mở thẻ mới)* | Hệ thống phát hành thẻ (External) bắn event. `transaction-service` kiểm tra khóa bảo vệ (Idempotency) để đảm bảo chỉ thưởng 1 lần. | Hệ thống phát hành thẻ, `transaction-service`, Kafka (`events-topic`) |
+| **UC-07** - Update Profile
*(Hoàn tất hồ sơ)* | `profile-service` tự đánh giá độ hoàn thiện và bắn event. Core engine kiểm tra mốc thời gian (trước 30/09) để quyết định cộng điểm. | `profile-service`, `transaction-service`, Kafka (`events-topic`) |
+| **UC-08** - Survey
*(Hoàn thành khảo sát)* | Bắn 1 event nhưng định tuyến song song 2 luồng: (1) Cấp voucher và (2) Cập nhật cờ `survey_completed = true` để không hiển thị lại popup. | `transaction-service`, `attribute-service`, Kafka (`events-topic`, `tracking-topic`) |
+| **UC-09** - Redeem Item
*(Đổi quà)* | Kiểm tra số dư, trừ điểm và khóa tồn kho (reserve item) trong 1 giao dịch cơ sở dữ liệu nguyên tử (ACID DB Transaction). | `transaction-service`, `notification-service`, PostgreSQL (Txn DB). Đã có luồng xử lý, không cần đi qua events |
+| **UC-10** - Merchant Scan
*(Quét mã voucher)* | API Đồng bộ (Synchronous). Đổi trạng thái voucher sang USED (ACID). Thành công sẽ đẩy noti realtime cho người dùng qua Kafka. | Merchant App, `transaction-service`, API Gateway, PostgreSQL (Txn DB). Đã có luồng xử lý, không cần đi qua events |
+| **UC-11** - Retention
*(Mật khẩu sắp hết hạn)* | Scheduler chạy hằng ngày quét DB lọc danh sách `password_expiry_date`. Bắn thẳng thông báo vào Kafka mà không cần đi qua Core engine. | `marketing-service`, `notification-service`, Kafka (`noti-topic`). Đã có luồng xử lý, không cần đi qua events |
+| **UC-12** - Most Redeem Item
*(Sản phẩm đổi nhiều nhất)* | Event Processor sao chép luồng sự kiện đổi quà sang `tracking-topic` để cập nhật counter `total_redeem_count` trên thuộc tính sản phẩm (Thuần Tracking). | Event Processor, `attribute-service`, Kafka (`tracking-topic`), PostgreSQL (Attr DB) |
+| **UC-13** - Retention
*(User Dormant 3 tháng)* | Scheduler chạy hằng ngày lọc user có `last_user_txn_date` >= 90 ngày. Bắn event vào Core engine để thực thi cấp ưu đãi riêng và push noti. | `marketing-service`, `transaction-service`, PostgreSQL (Txn DB & Marketing DB). Đã có luồng xử lý, không cần đi qua events |
+
+#### 4.3. Phân nhóm giải pháp kỹ thuật
+
+##### Nhóm 1: Hệ thống đếm và Theo dõi (Tracking & Counters)
+*Bao gồm: UC-01, UC-02, UC-03, UC-05, UC-12*
+- **Luồng dữ liệu:** Client -> `event-processor-service` -> `tracking-topic` -> `attribute-service`.
+- **Pattern "Accumulate & Trigger":** `attribute-service` hoạt động như một "Dumb Executor" tích lũy các event có tần suất cao (như Login, ViewItem). Khi đạt ngưỡng (Threshold) hoặc điều kiện trạng thái, nó mới sản sinh một event "có giá trị tài chính" đẩy vào `events-topic` để `transaction-service` trả thưởng.
+- **Caching:** Dùng Redis để lưu trữ thuộc tính `recently_viewed` (UC-03) — độ trễ <10ms.
+- **Idempotency:** Bộ đếm tự động lọc bỏ các hành động lặp lại không hợp lệ, ví dụ login nhiều lần trong 1 ngày (UC-02) hoặc kiểm tra cờ (UC-01).
+
+##### Nhóm 2: Giao dịch Tài chính & Quản lý Kho (Financial & Inventory)
+*Bao gồm: UC-04, UC-09, UC-10*
+- **Luồng dữ liệu:**
+ - (UC-04, UC-09): Client -> `event-processor-service` -> `events-topic` -> `transaction-service`.
+ - (UC-10): Merchant App -> HTTP Sync API -> `transaction-service`.
+- **ACID Transactions / Rollback:** Việc trừ điểm người gửi & cộng điểm người nhận (UC-04) hoặc trừ điểm & khóa tồn kho (UC-09) bắt buộc nằm chung trong 1 Database Transaction nguyên tử.
+- **Sync API:** UC-10 thực thi trực tiếp qua Restful API để trả về kết quả ngay lập tức trên máy POS của Merchant.
+
+##### Nhóm 3: Tương tác Hệ thống Ngoài & Luồng Song song
+*Bao gồm: UC-06, UC-07, UC-08*
+- **Luồng dữ liệu:** External Systems / App -> `event-processor-service` -> `events-topic` -> `transaction-service`.
+- **Dual Publishing:** Trong UC-08, Event Processor sao chép sự kiện thành 2 luồng: `events-topic` (lấy voucher) + `tracking-topic` (ẩn màn hình khảo sát). Giảm liên kết cứng (decouple) giữa UI State và Financial Ledger.
+
+##### Nhóm 4: Cronjob, Batch Processing & Retention
+*Bao gồm: UC-11, UC-13*
+- **Luồng dữ liệu:** `marketing-service` (Cronjob) -> Query Database -> Kafka -> Các Service liên quan.
+- **Off-peak Processing:** Chạy các job quét CSDL vào giờ thấp điểm (đêm khuya).
+- **Phân luồng:** UC-11 chỉ gửi tin nhắn → `noti-topic`. UC-13 liên quan đến tặng quà → `events-topic`.
+
+---
+
+### 5. Attribute Service — Operation Catalog
+
+Attribute Service hỗ trợ một tập cố định các **Primitive Operations** (Toán tử nguyên thủy). Marketing/BA có thể lắp ghép các toán tử này để tạo ra vô số Campaign mà không cần Dev sửa code.
+
+#### 5.1. Quy tắc chung
+
+- **Timezone:** Mọi phép so sánh ngày/giờ đều được convert về `Asia/Ho_Chi_Minh` trước khi xử lý.
+- **Time Source:** Luôn dùng **Event Time** (thời điểm sự kiện xảy ra tại Client/Source, nằm trong payload) — KHÔNG dùng Processing Time (thời điểm Attribute Service nhận event). Điều này đảm bảo kết quả deterministic, không phụ thuộc vào tải hệ thống hay Kafka lag.
+- **Key chưa tồn tại:** Khi nhận Instruction cho một Key chưa có trong DB, Attribute Service tự khởi tạo giá trị mặc định rồi thực thi Operation.
+- **Concurrency:** Kafka topic `tracking-topic` PHẢI partition theo `customerId`. Mọi event của cùng một user luôn vào cùng một partition → được xử lý tuần tự bởi một consumer duy nhất → không xảy ra race condition.
+- **Threshold Trigger:** Mặc định `triggerOnce: true`. Mỗi threshold chỉ fire đúng 1 lần khi value lần đầu thỏa mãn condition. Attribute Service tra cứu bảng `threshold_fire_log` (theo `attribute_key` + `threshold_id`) để ngăn trigger lặp lại.
+
+#### 5.2. Attribute Record
+
+Mỗi Key trong Attribute Service được lưu trữ với cấu trúc sau:
+
+```text
+┌─ Attribute Record ──────────────────────────────────┐
+│ key: "user:CUST-001:login_streak" │
+│ value: 5 (Number/String/Bool) │
+│ last_updated: "2026-07-15T23:50:00+07:00" │
+│ created_at: "2026-07-10T08:00:00+07:00" │
+└──────────────────────────────────────────────────────┘
+```
+
+- `last_updated` được Attribute Service tự cập nhật (bằng Event Time) mỗi khi có write thành công. Đây là nền tảng để xử lý mọi bài toán thời gian.
+
+#### 5.2b. Threshold Fire Log (Bảng lịch sử kích hoạt Threshold)
+
+Vì **1 attribute có thể phục vụ nhiều threshold khác nhau** (ví dụ: `login_streak` có threshold `== 7` để thưởng 100đ VÀ threshold `== 30` để thưởng 500đ).
+
+```text
+┌─ Threshold Fire Log ────────────────────────────────────────────────────┐
+│ id: (auto) │
+│ customer_id: "CUST-001" │
+│ attribute_key: "login_streak" │
+│ threshold_id: "Login_Streak_7_Days" │
+│ event_id: "evt-2026-07-16-abc123" (Event gốc gây trigger) │
+│ fired_at: "2026-07-16T10:30:00+07:00" (Event Time) │
+│ created_at: "2026-07-16T10:30:05+07:00" (Processing Time) │
+└─────────────────────────────────────────────────────────────────────────┘
+```
+
+| Field | Mô tả |
+|:---|:---|
+| `customer_id` | ID khách hàng sở hữu attribute |
+| `attribute_key` | Key của attribute đã thỏa mãn điều kiện |
+| `threshold_id` | Định danh threshold (trùng với `triggerEvent` trong Instruction). Dùng làm **Unique Constraint** cùng `customer_id` + `attribute_key` để đảm bảo `triggerOnce` |
+| `event_id` | ID của event gốc (từ Client) đã gây ra trigger — phục vụ traceability và debug |
+| `fired_at` | Thời điểm sự kiện xảy ra (Event Time) — dùng để audit |
+
+**Quy trình kiểm tra `triggerOnce`:**
+1. Sau khi Operation thực thi xong và value thay đổi, evaluate threshold condition.
+2. Nếu thỏa mãn → **Tra cứu** `threshold_fire_log` với composite key `(customer_id, attribute_key, threshold_id)`.
+3. Nếu **chưa có record** → INSERT log + Publish Trigger Event.
+4. Nếu **đã có record** → Bỏ qua (threshold đã fire trước đó).
+
+```text
+Ví dụ: login_streak có 2 thresholds
+
+ Threshold A: { "==": 7, triggerEvent: "Login_Streak_7_Days" }
+ Threshold B: { "==": 30, triggerEvent: "Login_Streak_30_Days" }
+
+ Ngày 7: login_streak = 7
+ → Check A: 7 == 7 ✓, log chưa có → INSERT log + Trigger "Login_Streak_7_Days" ✓
+ → Check B: 7 == 30 ✗ → Bỏ qua
+
+ Ngày 30: login_streak = 30
+ → Check A: 30 == 7 ✗ → Bỏ qua
+ → Check B: 30 == 30 ✓, log chưa có → INSERT log + Trigger "Login_Streak_30_Days" ✓
+
+ Ngày 31: login_streak = 31 (streak reset rồi đếm lại → giả sử đạt 30 lần nữa)
+ → Check B: 30 == 30 ✓, log ĐÃ CÓ → Bỏ qua. Không thưởng lần 2.
+```
+
+#### 5.3. Phase 1 — Operations cần thiết cho 13 Use Cases hiện tại
+
+##### `SET`
+
+Ghi đè giá trị vô điều kiện (Overwrite).
+
+| Thuộc tính | Giá trị |
+|:---|:---|
+| Kiểu value | `String`, `Number`, `Boolean`, `Date` |
+| Key chưa tồn tại | Tạo mới với value được truyền vào |
+| Idempotent | Không. Mỗi lần gọi đều ghi đè |
+
+**Use Case:** UC-05 — Mỗi lần User login, cập nhật `SET last_login_date = eventTime`. Kết hợp với Threshold `TIME_GAP_GT` để phát hiện khoảng cách ngủ đông.
+
+**Hành vi chi tiết:**
+1. Nếu có threshold `TIME_GAP_GT`: Kiểm tra threshold **TRƯỚC** khi ghi đè. So sánh `(eventTime - old_value)`. Nếu thỏa mãn → sinh Trigger Event. Sau đó mới ghi đè value mới.
+2. Nếu không có threshold: Ghi đè trực tiếp.
+
+```text
+Ví dụ UC-05 (Login sau 90 ngày ngủ đông):
+
+ Trạng thái hiện tại: last_login_date = "2026-04-15"
+ Event đến: eventTime = "2026-07-16"
+
+ Bước 1: Check threshold TIME_GAP_GT 90 days
+ → (2026-07-16) - (2026-04-15) = 92 ngày > 90 ✓
+ → Publish Trigger Event: "Account_Reactivated"
+ Bước 2: SET last_login_date = "2026-07-16"
+```
+
+##### `SET_TRUE_ONCE`
+
+Đánh dấu cờ Boolean = `true`. Chỉ có hiệu lực **đúng 1 lần duy nhất** khi key chưa tồn tại hoặc đang là `false`/`null`. Đảm bảo **Idempotency** (tính lũy đẳng) tuyệt đối.
+
+| Thuộc tính | Giá trị |
+|:---|:---|
+| Kiểu value | `Boolean` (cố định) |
+| Key chưa tồn tại | Tạo mới = `true`, sinh Trigger Event |
+| Key đã là `true` | **Bỏ qua hoàn toàn** — không ghi, không trigger |
+
+**Use Cases:** UC-01 (First Login), UC-08 (Survey Completed).
+
+**Hành vi chi tiết:**
+```text
+Ví dụ UC-01 (First Login):
+
+ Lần 1: key "first_login" chưa tồn tại
+ → Tạo mới: value = true
+ → Trigger Event: "First_Login" ✓
+
+ Lần 2: key "first_login" = true (đã tồn tại)
+ → Bỏ qua. Không ghi, không trigger.
+ → User KHÔNG nhận thưởng lần 2. An toàn.
+```
+
+##### `INCREMENT`
+
+Cộng dồn giá trị số. Hỗ trợ 2 chế độ thời gian tùy chọn: **Streak** (chuỗi liên tiếp) và **Window** (cửa sổ chu kỳ).
+
+| Thuộc tính | Giá trị |
+|:---|:---|
+| Kiểu value | `Number` |
+| Key chưa tồn tại | Tạo mới = `0`, rồi thực thi `+val` → kết quả = `val` |
+| Tham số bắt buộc | `val` (số cần cộng, thường = 1) |
+| Tham số tùy chọn | `streak` hoặc `window` (loại trừ nhau) |
+
+**Chế độ 1: Không có streak/window** — Cộng dồn thuần túy, không reset.
+
+```text
+Ví dụ UC-12 (Most Redeem — Bộ đếm tích lũy vĩnh viễn):
+
+ INCREMENT total_redeem_count, val: 1
+
+ Lần 1: 0 + 1 = 1
+ Lần 2: 1 + 1 = 2
+ ...
+ Lần N: (N-1) + 1 = N
+
+ Không bao giờ reset. Dùng để thống kê, ranking.
+```
+
+**Chế độ 2: `streak` — Đếm chuỗi liên tiếp.**
+
+Dùng để đếm số ngày/tuần/tháng liên tiếp mà user thực hiện hành vi. Nếu **đứt chuỗi** (bỏ qua 1 chu kỳ), reset về 1 và đếm lại.
+
+Giá trị `streak` hợp lệ: `DAILY`, `WEEKLY`, `MONTHLY`.
+
+Logic xử lý (dựa trên `last_updated` và `eventTime`, đã convert về `Asia/Ho_Chi_Minh`):
+
+```text
+streak: "DAILY"
+
+ Tính khoảng cách ngày giữa eventTime và last_updated:
+
+ ┌──────────────────────┬──────────────────────────────────────────┐
+ │ Khoảng cách │ Hành vi │
+ ├──────────────────────┼──────────────────────────────────────────┤
+ │ Cùng ngày │ Bỏ qua (Idempotent — login 5 lần/ngày │
+ │ │ chỉ đếm 1) │
+ │ Đúng 1 ngày trước │ value = value + val (streak tiếp tục) │
+ │ Cách > 1 ngày │ value = 1 (gán thẳng — streak đứt, │
+ │ │ bắt đầu chuỗi mới từ hôm nay) │
+ │ Key chưa tồn tại │ value = 1 (bắt đầu chuỗi mới) │
+ └──────────────────────┴──────────────────────────────────────────┘
+```
+
+> **Lưu ý quan trọng:** Khi streak đứt, giá trị được gán thẳng = 1 trong **một thao tác atomic duy nhất** (không phải gọi RESET rồi INCREMENT riêng). Vì hôm nay user CÓ thực hiện hành vi, nên streak phải là 1 chứ không phải 0.
+
+```text
+Ví dụ UC-02 (Daily Login Streak):
+
+ INCREMENT login_streak, val: 1, streak: "DAILY"
+ Threshold: { "==": 7, triggerEvent: "Login_Streak_7_Days" }
+
+ Ngày 1 (Thứ Hai): key mới → value = 1
+ Ngày 2 (Thứ Ba): hôm qua → value = 2
+ Ngày 3 (Thứ Tư): hôm qua → value = 3
+ Ngày 3 (Thứ Tư, login lần 2): cùng ngày → BỎ QUA
+ Ngày 4 (Thứ Năm): hôm qua → value = 4
+ --- User không login Thứ Sáu ---
+ Ngày 6 (Thứ Bảy): cách 2 ngày → value = 1 (streak đứt, đếm lại)
+ Ngày 7 (Chủ Nhật): hôm qua → value = 2
+ ...
+ Ngày N (liên tiếp 7): hôm qua → value = 7 → Trigger! ✓
+```
+
+**Chế độ 3: `window` — Đếm gộp trong chu kỳ cố định.**
+
+Dùng để đếm tổng số lần thực hiện hành vi trong một khoảng thời gian. Hết chu kỳ thì reset, bắt đầu đếm lại từ đầu. **Không quan tâm liên tiếp hay không.**
+
+Giá trị `window` hợp lệ: `DAILY`, `WEEKLY`, `MONTHLY`.
+
+```text
+window: "WEEKLY"
+
+ Tính xem eventTime và last_updated có cùng chu kỳ không:
+
+ ┌──────────────────────┬──────────────────────────────────────────┐
+ │ Điều kiện │ Hành vi │
+ ├──────────────────────┼──────────────────────────────────────────┤
+ │ Cùng tuần (ISO 8601) │ value = value + val (tiếp tục đếm) │
+ │ Khác tuần │ value = 1 (reset, bắt đầu chu kỳ mới) │
+ │ Key chưa tồn tại │ value = 1 (bắt đầu chu kỳ mới) │
+ └──────────────────────┴──────────────────────────────────────────┘
+```
+
+> **Quy ước chu kỳ:** `WEEKLY` tính theo ISO 8601 (Thứ Hai là ngày đầu tuần). `MONTHLY` tính theo tháng dương lịch (ngày 1 là ngày đầu tháng).
+
+```text
+Ví dụ UC-03 (View Item — 10 lần xem trong tuần):
+
+ INCREMENT weekly_view_count, val: 1, window: "WEEKLY"
+ Threshold: { "==": 10, triggerEvent: "View_10_Items_Weekly" }
+
+ Thứ Hai: xem 3 item → value = 3
+ Thứ Tư: xem 4 item → value = 7
+ Thứ Sáu: xem 3 item → value = 10 → Trigger! ✓
+ --- Sang tuần mới (Thứ Hai tiếp theo) ---
+ Thứ Hai: xem 1 item → value = 1 (reset, đếm lại từ đầu)
+```
+
+##### `APPEND`
+
+Thêm phần tử vào cuối danh sách (List/Array).
+
+| Thuộc tính | Giá trị |
+|:---|:---|
+| Kiểu value | `Array` |
+| Key chưa tồn tại | Tạo mới = `[]`, rồi thêm phần tử |
+| Tham số tùy chọn | `unique` (Boolean), `maxSize` (Number) |
+
+**Options:**
+- `unique: true` — Chỉ thêm nếu phần tử chưa có trong danh sách. Bỏ qua nếu trùng.
+- `maxSize: N` — Giới hạn kích thước tối đa. Khi đạt `maxSize`, xóa phần tử **cũ nhất** (FIFO) để nhường chỗ cho phần tử mới.
+
+```text
+Ví dụ UC-03 (Lưu vết sản phẩm đã xem):
+
+ APPEND viewed_items, val: "item_789", unique: true, maxSize: 50
+
+ Bước 1: viewed_items = ["item_123", "item_456"]
+ → Thêm "item_789"
+ → viewed_items = ["item_123", "item_456", "item_789"]
+
+ Trùng lặp: APPEND viewed_items, val: "item_123", unique: true
+ → "item_123" đã có → BỎ QUA
+
+ Tràn maxSize: list có 50 items, thêm "item_new"
+ → Xóa phần tử cũ nhất (FIFO): "item_001"
+ → Thêm "item_new" vào cuối
+ → list vẫn = 50 items
+```
+
+#### 5.4. Threshold (Ngưỡng kích hoạt)
+
+Threshold là điều kiện để Attribute Service quyết định có publish Trigger Event hay không. Mỗi Instruction có thể đính kèm **tối đa 1 threshold**.
+
+**Operators hỗ trợ:**
+
+| Operator | Ý nghĩa | Ví dụ |
+|:---|:---|:---|
+| `==` | Bằng | `login_streak == 7` |
+| `>=` | Lớn hơn hoặc bằng | `total_spend >= 5000000` |
+| `>` | Lớn hơn | `view_count > 10` |
+| `TIME_GAP_GT` | Khoảng cách thời gian lớn hơn N ngày (so sánh `eventTime - old_value`) | `last_login_date` cách `eventTime` > 90 ngày |
+
+**Quy tắc trigger:**
+- Mặc định `triggerOnce: true`: Threshold chỉ fire **1 lần duy nhất** khi value lần đầu thỏa mãn condition. Attribute Service tra cứu bảng `threshold_fire_log` (composite key: `customer_id` + `attribute_key` + `threshold_id`) để ngăn trigger lặp lại. Chi tiết xem [§5.2b](#52b-threshold-fire-log-bảng-lịch-sử-kích-hoạt-threshold).
+- Threshold chỉ được evaluate khi **value thực sự thay đổi**. Nếu operation bị bỏ qua (VD: `SET_TRUE_ONCE` khi đã true, hoặc `INCREMENT` với `streak` cùng ngày), threshold không được evaluate.
+
+---
+
+### 6. Instruction Format & Registry
+
+#### 6.1. Instruction Format (Định dạng Chỉ thị)
+
+Mỗi Instruction trong Fat Payload tuân theo cấu trúc chuẩn sau:
+
+```json
+{
+ "op": "INCREMENT",
+ "key": "login_streak",
+ "val": 1,
+ "streak": "DAILY",
+ "options": {},
+ "threshold": {
+ "condition": "==",
+ "target": 7,
+ "triggerOnce": true,
+ "triggerEvent": "Login_Streak_7_Days"
+ }
+}
+```
+
+| Field | Bắt buộc | Mô tả |
+|:---|:---|:---|
+| `op` | ✓ | Tên Operation: `SET`, `SET_TRUE_ONCE`, `INCREMENT`, `APPEND` |
+| `key` | ✓ | Tên attribute key (không bao gồm prefix `customerId`, prefix được tự động thêm bởi Attribute Service) |
+| `val` | ✓ | Giá trị truyền vào (Number, String, Boolean tùy `op`) |
+| `streak` | | Chế độ đếm chuỗi liên tiếp. Giá trị: `DAILY`, `WEEKLY`, `MONTHLY`. Chỉ dùng với `INCREMENT`. **Loại trừ với `window`** |
+| `window` | | Chế độ đếm gộp theo chu kỳ. Giá trị: `DAILY`, `WEEKLY`, `MONTHLY`. Chỉ dùng với `INCREMENT`. **Loại trừ với `streak`** |
+| `options` | | Tùy chọn bổ sung. Với `APPEND`: `{ "unique": true, "maxSize": 50 }` |
+| `threshold` | | Điều kiện kích hoạt Trigger Event. Nếu không có thì Attribute chỉ lưu data, không trigger |
+
+#### 6.2. Registry (Cơ chế Đăng ký Sự kiện)
+
+Toàn bộ "trí tuệ" phân luồng nằm tại cấu hình (Registry) của Event Processor.
+
+**Luồng ánh xạ:** `Thin Event (Client gửi)` ──> `Registry tra cứu` ──> `Fat Payload (Chứa Instructions)`.
+
+*Ví dụ Registry:* Khi Client gửi sự kiện `Login`, Registry cấu hình đính kèm 3 Instructions:
+
+1. `SET_TRUE_ONCE` cho cờ `first_login` — Trigger `First_Login` nếu lần đầu.
+2. `INCREMENT` với `streak: "DAILY"` cho `login_streak` — Trigger `Login_Streak_7_Days` khi đạt 7.
+3. `SET` cho `last_login_date = eventTime` — Trigger `Account_Reactivated` nếu `TIME_GAP_GT 90 days`.
+
+---
+
+### 7. Non-Functional Requirements & Risk Assessment
+
+#### 7.1. Hiệu năng & Mở rộng (Performance & Scalability)
+
+- **Tách bạch luồng dữ liệu (Isolation):** Việc tách riêng luồng giao dịch cốt lõi (Direct) và luồng theo dõi hành vi (Tracking) giúp Core Engine tập trung 100% công suất cho dòng tiền/điểm.
+- **Event-Driven & Async processing:** Các luồng không yêu cầu phản hồi ngay được đẩy qua Kafka, cho phép các service tự điều chỉnh tốc độ tiêu thụ (throttle) và dễ dàng mở rộng ngang (horizontal scaling) thông qua việc tăng partition của Kafka.
+
+#### 7.2. Bảo mật & Tính toàn vẹn (Security & Data Integrity)
+
+- **Idempotency (Tính luỹ đẳng):** Transaction Service đảm bảo mọi giao dịch cộng/trừ điểm hoặc cấp thẻ/voucher đều kiểm tra khóa Idempotent, chống duplicate event sinh ra từ mạng chập chờn.
+- **ACID Transaction:** Mọi biến động điểm số hay trạng thái kho (voucher) đều được thực thi và khóa chặt trong 1 Database Transaction nguyên tử.
+
+#### 7.3. Tính khả dụng & Giám sát (Availability & Monitoring)
+
+- **Smart Routing & Fallback:** Event Processor kiểm tra định dạng Schema của từng sự kiện đầu vào. Các sự kiện lỗi định dạng sẽ bị reject hoặc đẩy vào Dead Letter Queue (DLQ) để điều tra và phục hồi sau.
+
+#### 7.4. Đánh giá Rủi ro Kỹ thuật (Technical Risk Assessment)
+
+**1. Rủi ro phức tạp nhất: Đổi quà (UC-09 - Redeem Item)**
+- **Vấn đề:** Rất dễ xảy ra lỗi tranh chấp dữ liệu (Race Condition) khi nhiều người dùng cùng đổi 1 món quà đang "hot" có số lượng giới hạn, hoặc timeout giao dịch DB khiến hệ thống "trừ điểm nhưng kho không trừ".
+- **Phương án dự phòng:**
+ - Áp dụng cơ chế **Pessimistic Locking** (`SELECT FOR UPDATE`) ở bảng Inventory.
+ - Kết hợp sử dụng **Redis Decrement** để cấp phát số lượng kho tạm thời trên memory trước khi thực hiện giao dịch xuống Database (Pre-deduction).
+
+**2. Nghẽn cổ chai luồng sự kiện (Bottleneck in High-Frequency Events)**
+- **Vấn đề:** Các sự kiện như `View_Item` hay `Login` có lượng truy cập khổng lồ, nếu đẩy dồn dập có thể làm chậm trễ các sự kiện quan trọng như `Transfer_Balance` trên hạ tầng Kafka.
+- **Phương án dự phòng:**
+ - Tách bạch hoàn toàn `tracking-topic` (Volume lớn, độ ưu tiên thấp) và `events-topic` (Volume vừa, độ ưu tiên cao).
+ - Tăng số lượng Partitions trên `tracking-topic` và chạy nhiều Instance (Consumer Group) của `attribute-service` để tiêu thụ song song (Horizontal Scale).
+
+**3. Cơ chế phục hồi sự cố (Fault Tolerance & Dead Letter Queue)**
+- **Vấn đề:** Khi `transaction-service` dính ngoại lệ logic (VD: Khách hàng bị khóa tài khoản ngay lúc event trả điểm tới) khiến việc consume báo lỗi liên tục.
+- **Phương án dự phòng:**
+ - Cấu hình **Dead Letter Queue (DLQ)** trên Kafka. Nếu một sự kiện xử lý thất bại sau số lần retry quy định (VD: 3 lần), hệ thống sẽ gắp nó bỏ qua DLQ để tránh làm nghẽn dòng thông tin (Head-of-line blocking). Đội vận hành sẽ tái xử lý (replay) các sự kiện DLQ này sau khi khắc phục xong lỗi.
+
+---
+
+### Appendix: Bảng tóm tắt 13 Use Cases
+
+| Use Case | Original Event | Type | Operation | Consumer | Output |
+| :--- | :--- | :--- | :--- | :--- | :--- |
+| **UC-01** First Login | `Login` | Tracking | `SET_TRUE_ONCE` | `Attribute` ➔ `Transaction` | +100đ, In-app msg, OTT |
+| **UC-02** Daily Login | `Login` | Tracking | `INCREMENT` (streak: DAILY) | `Attribute` ➔ `Transaction` | +100đ / +500đ |
+| **UC-03** View Item | `ViewItem` | Tracking | `INCREMENT` (window: WEEKLY) + `APPEND` | `Attribute` ➔ `Transaction` | +50đ / Lưu log recommend |
+| **UC-04** Transfer | `Transfer_Balance` | Direct | N/A | `Transaction` | Debit/Credit ACID |
+| **UC-05** Inactive Login | `Login` | Tracking | `SET` (TIME_GAP_GT 90d) | `Attribute` ➔ `Transaction` | Trigger Account_Reactivated |
+| **UC-06** Welcome Offer | `Welcome_Offer` | Direct | N/A | `Transaction` | +100đ |
+| **UC-07** Update Profile | `Profile_Completed` | Direct | N/A | `Transaction` | +100đ |
+| **UC-08** Survey | `Survey_Completed` | Tracking | `SET_TRUE_ONCE` | `Attribute` ➔ `Transaction` | Tặng voucher |
+| **UC-09** Redeem | `Redeem_Item` | Direct | N/A | `Transaction` | Trừ điểm + Reserve item |
+| **UC-10** Merchant Scan | N/A (Sync API) | Sync | N/A | `Transaction` | Đánh dấu voucher USED |
+| **UC-11** Retention PW | `Cronjob_Run` | Notification | N/A | `Notification` | Push/Email cảnh báo PW |
+| **UC-12** Most Redeem | `Redeem_Item` | Tracking | `INCREMENT` (tích lũy) | `Attribute` | Tăng bộ đếm thuần túy |
+| **UC-13** Dormant User | `Cronjob_Run` | Direct | N/A | `Transaction` | Tặng ưu đãi + Push Noti |
diff --git a/specs/Event_Processor_HLD_v2.md b/specs/Event_Processor_HLD_v2.md
new file mode 100644
index 0000000..200bd30
--- /dev/null
+++ b/specs/Event_Processor_HLD_v2.md
@@ -0,0 +1,804 @@
+# Event Processor Service — High-Level Design v2
+## Event-Driven Loyalty & Rewards Platform
+
+---
+
+### 1. Mục tiêu & Phạm vi
+
+#### 1.1. Mục tiêu
+
+- **Chuẩn hóa luồng xử lý event:** Các event phi tài chính đi qua `event-processor-service` để validate, enrich và route.
+- **Bảo vệ Transaction Service (TP):** TP chỉ xử lý command/event có tác động tài chính, voucher, inventory hoặc reward rule.
+- **Tách tracking khỏi core ledger:** Các counter, streak, trạng thái hiển thị và thuộc tính động được xử lý tại `attribute-service`.
+- **Đảm bảo đúng dữ liệu tài chính:** Mọi nghiệp vụ cộng/trừ điểm, cấp voucher, đổi trạng thái voucher, reserve inventory phải nằm trong `transaction-service` và transaction DB.
+- **Cho phép cấu hình chiến dịch phổ biến mà không cần sửa code:** Operation catalog chỉ giữ các primitive cần thiết cho phần lớn use case hiện tại, tránh tạo quá nhiều operation sớm.
+
+#### 1.2. Nguyên tắc thiết kế
+
+1. **Direct Flow khác Direct Event.**
+ - **Direct Flow:** Command cần phản hồi ngay cho user/merchant và cần ACID mạnh. Client gọi thẳng API của `transaction-service`.
+ - **Direct Event:** Event bất đồng bộ cần TP xử lý rule/reward, nhưng không cần response sync cho caller. Producer publish vào Kafka `events-topic`.
+2. **Transaction Service** là nơi duy nhất thực hiện nghiệp vụ tài chính: cộng/trừ điểm, cấp voucher, reserve inventory, dùng voucher, giới hạn thưởng.
+3. **Attribute Service** chỉ lưu state và chạy primitive operations. Service này không quyết định "thưởng bao nhiêu"; khi điều kiện tracking đạt threshold, nó phát sinh **một Direct Event mới** để `transaction-service` xử lý rule/reward.
+4. **Event Processor** là smart gateway cho event async: validate schema, tra registry, enrich payload, route vào topic tương ứng.
+5. **Marketing Service** chỉ lập lịch, quét tập khách hàng và phát sinh event/notification. Không cộng điểm hay cấp voucher trực tiếp.
+6. **Mọi publish quan trọng sau khi ghi DB phải dùng Outbox Pattern** để tránh mất event hoặc duplicate do crash/retry.
+
+---
+
+### 2. Kiến trúc Tổng quan
+
+```mermaid
+graph TD
+ subgraph Client Layer
+ MA[Member App]
+ MRA[Merchant App]
+ EXT[External Systems]
+ end
+
+ subgraph Gateway Layer
+ EP[event-processor-service
Validate + Registry + Route]
+ API[TP Sync API]
+ end
+
+ subgraph Kafka
+ ET[events-topic
Direct Events / Reward Triggers]
+ TT[tracking-topic
Tracking Events]
+ NT[noti-topic
Notification Events]
+ DLQ[dead-letter-topics]
+ end
+
+ subgraph Core Services
+ TP[transaction-service
Rule + Ledger + Inventory]
+ ATTR[attribute-service
State + Counters]
+ NS[notification-service]
+ MKT[marketing-service]
+ end
+
+ subgraph Data Layer
+ DB_TP[(PostgreSQL
Transaction DB)]
+ DB_ATTR[(PostgreSQL
Attribute DB)]
+ REDIS[(Redis
Cache)]
+ DORIS[(Doris
BI / Reporting)]
+ end
+
+ MA -->|Tracking / Notification Events| EP
+ EXT -->|Async Direct Events| EP
+ MKT -->|Scheduled Events| EP
+
+ MA -->|Direct Flow: Redeem / Transfer| API
+ MRA -->|Direct Flow: Scan Voucher| API
+ API --> TP
+
+ EP -->|Direct Event| ET
+ EP -->|Tracking Event| TT
+ EP -->|Notification Event| NT
+
+ ET --> TP
+ TT --> ATTR
+ NT --> NS
+
+ ATTR -->|Threshold Trigger
via Outbox| ET
+ TP -->|Reward / Txn Result
via Outbox| NT
+ TP -->|Post-transaction Tracking Event
via Outbox| TT
+
+ TP --- DB_TP
+ ATTR --- DB_ATTR
+ ATTR -. recently viewed .-> REDIS
+ DB_TP -. CDC/ETL .-> DORIS
+ DB_ATTR -. CDC/ETL .-> DORIS
+
+ ET -. failed .-> DLQ
+ TT -. failed .-> DLQ
+ NT -. failed .-> DLQ
+```
+
+#### 2.1. Event Categories
+
+| Category | Khi nào dùng | Đường đi chính | Ví dụ |
+|:---|:---|:---|:---|
+| **Direct Flow** | User/merchant cần kết quả ngay, có ACID tài chính/inventory | Client/Merchant -> `transaction-service` API | `Transfer_Balance`, `Redeem_Item`, `Scan_Voucher` |
+| **Direct Event** | Async event cần TP xử lý reward/rule, không cần sync response. Có thể đến từ producer bên ngoài hoặc được sinh ra bởi Attribute khi Tracking Event đạt threshold | Producer -> EP -> `events-topic` -> TP; hoặc Attribute outbox -> `events-topic` -> TP | `Welcome_Offer`, `Profile_Completed`, `Dormant_User`, `Login_Streak_7_Days` |
+| **Tracking Event** | Cần đếm, tích lũy, set cờ, lưu trạng thái phi tài chính | Producer -> `event-processor-service` -> `tracking-topic` -> `attribute-service` | `Login`, `View_Item`, `Survey_Completed`, `Item_Redeemed_Count` |
+| **Notification Event** | Chỉ gửi thông báo, không cần ledger/counter | Producer -> `event-processor-service` hoặc service outbox -> `noti-topic` -> `notification-service` | `Password_Expiring`, transaction success notification |
+
+#### 2.2. Quan hệ Tracking Event và Direct Event phát sinh
+
+Tracking Event không trực tiếp yêu cầu TP cộng điểm/cấp voucher. Nó chỉ cập nhật state tại Attribute. Khi state đạt threshold, Attribute tạo ra **Generated Direct Event** mới và publish vào `events-topic`.
+
+Ví dụ:
+
+```text
+Tracking Event: Login
+ -> Attribute cập nhật login_streak = 7
+ -> Threshold Login_Streak_7_Days đạt
+ -> Attribute publish Direct Event: Login_Streak_7_Days
+ -> Transaction Service consume Direct Event và cộng điểm theo rule
+```
+
+Quy ước:
+
+- Direct Event phát sinh từ threshold phải có `eventCategory = "DIRECT"`.
+- `source` nên là `attribute-service`.
+- Payload phải giữ `sourceEventId` hoặc `causedByEventId` để trace ngược về Tracking Event gốc.
+- TP xử lý Generated Direct Event giống các Direct Event async khác: check idempotency, eligibility, campaign rule, rồi mới apply reward.
+
+---
+
+### 3. Luồng Xử lý Chuẩn
+
+#### 3.1. Tracking Flow — Accumulate & Trigger
+
+```mermaid
+sequenceDiagram
+ participant MA as Member App
+ participant EP as Event Processor
+ participant TT as tracking-topic
+ participant ATTR as Attribute Service
+ participant DB as Attribute DB
+ participant OUT as Attribute Outbox
+ participant ET as events-topic
+ participant TP as Transaction Service
+
+ MA->>EP: Thin Event: Login / View_Item
+ EP->>EP: Validate schema + load registry
+ EP->>TT: Fat Payload with instructions
+ TT->>ATTR: Consume
+ ATTR->>DB: BEGIN transaction
+ ATTR->>DB: Update attribute state
+ ATTR->>DB: Insert threshold_fire_log if threshold reached
+ ATTR->>OUT: Insert Generated Direct Event into outbox
+ ATTR->>DB: COMMIT
+ OUT->>ET: Publish Generated Direct Event
+ ET->>TP: Consume Direct Event and apply reward rule
+```
+
+**Điểm bắt buộc:**
+
+- Update attribute, insert `threshold_fire_log`, và insert outbox record phải nằm trong cùng DB transaction.
+- Generated Direct Event chỉ publish từ outbox sau khi commit thành công.
+- Generated Direct Event là event mới, có `eventId` riêng, nhưng phải tham chiếu Tracking Event gốc qua `causedByEventId`.
+- Nếu publish lỗi, outbox worker retry; không rollback state đã xử lý.
+
+#### 3.2. Direct Flow — Sync Financial Command
+
+Áp dụng cho nghiệp vụ user/merchant cần biết kết quả ngay: chuyển điểm, đổi quà, quét voucher.
+
+```mermaid
+sequenceDiagram
+ participant APP as Member/Merchant App
+ participant TP as Transaction Service
+ participant DB as Transaction DB
+ participant OUT as TP Outbox
+ participant NT as noti-topic
+ participant TT as tracking-topic
+
+ APP->>TP: Sync API command
+ TP->>DB: BEGIN transaction
+ TP->>DB: Validate balance / voucher / inventory
+ TP->>DB: Update ledger + inventory/voucher state
+ TP->>OUT: Insert notification/tracking events
+ TP->>DB: COMMIT
+ TP-->>APP: Return success/failure
+ OUT->>NT: Publish notification event
+ OUT->>TT: Publish post-transaction tracking event if needed
+```
+
+**Ví dụ:**
+
+- `Redeem_Item`: App gọi TP sync API. Sau khi redeem thành công, TP publish `Item_Redeemed_Count` vào `tracking-topic` để Attribute tăng `total_redeem_count`.
+- `Transfer_Balance`: App gọi TP sync API. Sau commit, TP publish notification cho người gửi và người nhận.
+- `Scan_Voucher`: Merchant App gọi TP sync API. Sau commit, TP publish notification cho customer.
+
+#### 3.3. Direct Event Flow — Async Reward/Rule
+
+Áp dụng cho event không cần trả kết quả tức thì cho caller nhưng cần TP xử lý rule/reward.
+
+```mermaid
+sequenceDiagram
+ participant P as Producer
+ participant EP as Event Processor
+ participant ET as events-topic
+ participant TP as Transaction Service
+ participant DB as Transaction DB
+ participant OUT as TP Outbox
+ participant NT as noti-topic
+
+ P->>EP: Direct Event: Welcome_Offer / Profile_Completed / Dormant_User
+ EP->>EP: Validate + route
+ EP->>ET: Publish Direct Event
+ ET->>TP: Consume
+ TP->>DB: BEGIN transaction
+ TP->>DB: Check idempotency + apply reward rule
+ TP->>OUT: Insert notification event
+ TP->>DB: COMMIT
+ OUT->>NT: Publish notification
+```
+
+Direct Event async có 2 nguồn hợp lệ:
+
+- **Producer-originated Direct Event:** service/hệ thống ngoài gửi event vào EP, ví dụ `Welcome_Offer`, `Profile_Completed`, `Dormant_User`.
+- **Threshold-generated Direct Event:** Attribute sinh event khi Tracking Event đạt threshold, ví dụ `First_Login_Reward`, `Login_Streak_7_Days`, `View_10_Items_Weekly`, `Survey_Completed_Reward`, `Account_Reactivated`.
+
+#### 3.4. Notification Flow
+
+- Notification thuần túy đi thẳng `noti-topic`.
+- Notification phát sinh từ transaction phải được TP ghi vào outbox cùng transaction DB.
+- Notification Service cần idempotency theo `eventId` để tránh gửi trùng khi Kafka retry.
+
+---
+
+### 4. Event Envelope Chuẩn
+
+Tất cả event đi qua Kafka nên có envelope thống nhất để trace, replay và idempotency.
+
+```json
+{
+ "eventId": "evt-20260716-abc123",
+ "eventName": "Login",
+ "eventCategory": "TRACKING",
+ "schemaVersion": 1,
+ "source": "member-app",
+ "occurredAt": "2026-07-16T10:30:00+07:00",
+ "publishedAt": "2026-07-16T10:30:02+07:00",
+ "correlationId": "corr-789",
+ "idempotencyKey": "member-login:CUST-001:2026-07-16",
+ "actor": {
+ "customerId": "CUST-001"
+ },
+ "entities": {
+ "itemId": "ITEM-789"
+ },
+ "payload": {}
+}
+```
+
+| Field | Bắt buộc | Mục đích |
+|:---|:---:|:---|
+| `eventId` | Yes | Unique event id, dùng cho dedup, DLQ, audit |
+| `eventName` | Yes | Tên event canonical, ví dụ `Login`, `View_Item` |
+| `eventCategory` | Yes | `DIRECT`, `TRACKING`, `NOTIFICATION` |
+| `schemaVersion` | Yes | Cho phép evolve schema |
+| `source` | Yes | Producer/service phát event |
+| `occurredAt` | Yes | Event time |
+| `publishedAt` | Yes | Processing/publish time |
+| `correlationId` | Yes | Trace một flow end-to-end |
+| `idempotencyKey` | Yes | Business dedup key |
+| `actor` | Theo event | Customer/member/merchant liên quan |
+| `entities` | Theo event | Item, voucher, card, survey... |
+| `payload` | Theo event | Data nghiệp vụ riêng |
+
+---
+
+### 5. Registry & Instruction Model
+
+#### 5.1. Registry
+
+Registry nằm tại `event-processor-service`, định nghĩa:
+
+- Event name và category.
+- Target topic.
+- Required fields/schema.
+- Instruction list cho tracking event.
+- Transaction code cho direct event.
+- Notification template cho notification event.
+
+Ví dụ registry cho `Login`:
+
+```json
+{
+ "eventName": "Login",
+ "eventCategory": "TRACKING",
+ "targetTopic": "tracking-topic",
+ "requiredFields": ["actor.customerId", "occurredAt"],
+ "partitionKeyExpression": "Customer:${actor.customerId}",
+ "instructions": [
+ {
+ "name": "First Login Bonus Gate",
+ "op": "SET_TRUE_ONCE",
+ "entity": "Customer",
+ "entityId": "${actor.customerId}",
+ "key": "first_login_flag",
+ "triggerEventOnSuccess": "First_Login_Reward"
+ },
+ {
+ "name": "Daily Login Streak",
+ "op": "INCREMENT",
+ "entity": "Customer",
+ "entityId": "${actor.customerId}",
+ "key": "login_streak",
+ "val": 1,
+ "mode": "STREAK",
+ "period": "DAILY",
+ "thresholds": [
+ {
+ "thresholdId": "Login_Streak_7_Days",
+ "evaluateOn": "AFTER_UPDATE",
+ "operator": "==",
+ "left": "CURRENT_VALUE",
+ "target": 7,
+ "triggerEvent": "Login_Streak_7_Days",
+ "triggerPolicy": "ONCE_LIFETIME"
+ },
+ {
+ "thresholdId": "Login_Streak_30_Days",
+ "evaluateOn": "AFTER_UPDATE",
+ "operator": "==",
+ "left": "CURRENT_VALUE",
+ "target": 30,
+ "triggerEvent": "Login_Streak_30_Days",
+ "triggerPolicy": "ONCE_LIFETIME"
+ }
+ ]
+ },
+ {
+ "name": "Inactive Account Monitor",
+ "op": "SET",
+ "entity": "Customer",
+ "entityId": "${actor.customerId}",
+ "key": "last_login_date",
+ "val": "${occurredAt}",
+ "thresholds": [
+ {
+ "thresholdId": "Account_Reactivated_90D",
+ "evaluateOn": "BEFORE_UPDATE",
+ "operator": "TIME_GAP_GT",
+ "left": "OLD_VALUE",
+ "right": "${occurredAt}",
+ "target": 90,
+ "unit": "DAY",
+ "triggerEvent": "Account_Reactivated",
+ "triggerPolicy": "ONCE_PER_PERIOD",
+ "period": "YEARLY"
+ }
+ ]
+ }
+ ]
+}
+```
+
+#### 5.2. Instruction Format
+
+```json
+{
+ "name": "Weekly View Count",
+ "op": "INCREMENT",
+ "entity": "Customer",
+ "entityId": "${actor.customerId}",
+ "key": "weekly_view_count",
+ "val": 1,
+ "mode": "WINDOW",
+ "period": "WEEKLY",
+ "thresholds": [
+ {
+ "thresholdId": "View_10_Items_Weekly",
+ "evaluateOn": "AFTER_UPDATE",
+ "operator": ">=",
+ "left": "CURRENT_VALUE",
+ "target": 10,
+ "triggerEvent": "View_10_Items_Weekly",
+ "triggerPolicy": "ONCE_PER_PERIOD",
+ "period": "WEEKLY"
+ }
+ ]
+}
+```
+
+| Field | Bắt buộc | Ghi chú |
+|:---|:---:|:---|
+| `name` | Yes | Tên dễ đọc cho BA/operation |
+| `op` | Yes | `SET`, `SET_TRUE_ONCE`, `INCREMENT`, `APPEND_UNIQUE` |
+| `entity` | Yes | `Customer`, `Item`, `Voucher`, `Survey`... |
+| `entityId` | Yes | ID object được update state |
+| `key` | Yes | Attribute key |
+| `val` | Theo op | Giá trị input |
+| `mode` | Optional | Chỉ dùng cho `INCREMENT`: `LIFETIME`, `WINDOW`, `STREAK` |
+| `period` | Optional | `DAILY`, `WEEKLY`, `MONTHLY`, `YEARLY` |
+| `thresholds` | Optional | Mảng threshold. Mỗi threshold tự khai báo `evaluateOn` là `BEFORE_UPDATE` hoặc `AFTER_UPDATE` |
+| `triggerEventOnSuccess` | Optional | Dùng cho state transition như `SET_TRUE_ONCE` |
+
+---
+
+### 6. Operation Catalog Phase 1
+
+Thiết kế phase 1 chỉ cần 4 primitive operations. Đây là bộ đủ cho phần lớn nghiệp vụ loyalty thực tế: first action, streak, periodic counter, total counter, timer gap và recent list.
+
+#### 6.1. `SET`
+
+Ghi đè giá trị attribute.
+
+| Thuộc tính | Giá trị |
+|:---|:---|
+| Kiểu value | String, Number, Boolean, DateTime |
+| Idempotent | Không mặc định; phụ thuộc `idempotencyKey` event |
+| Use cases | UC-05 `last_login_date`, các mốc thời gian/state đơn giản |
+
+Với bài toán inactive login, `SET` dùng threshold có `evaluateOn = BEFORE_UPDATE`: check khoảng cách giữa `occurredAt` và old value trước, sau đó mới ghi value mới.
+
+```text
+old last_login_date = 2026-04-15
+event occurredAt = 2026-07-16
+TIME_GAP_GT 90 days = true
+=> emit Account_Reactivated
+=> SET last_login_date = 2026-07-16
+```
+
+#### 6.2. `SET_TRUE_ONCE`
+
+Chuyển Boolean từ `false/null/not_exists` sang `true` đúng một lần.
+
+| Thuộc tính | Giá trị |
+|:---|:---|
+| Kiểu value | Boolean |
+| Idempotent | Có |
+| Trigger | Chỉ trigger khi state transition thành công |
+| Use cases | UC-01 First Login, UC-08 Survey Completed |
+
+Best practice: dùng operation này làm **gate** cho reward chỉ được nhận một lần. Với UC-08, chỉ khi `survey_completed` chuyển thành true thì mới emit `Survey_Completed_Reward`.
+
+#### 6.3. `INCREMENT`
+
+Cộng số theo 3 mode.
+
+| Mode | Ý nghĩa | Use cases |
+|:---|:---|:---|
+| `LIFETIME` | Counter tích lũy không reset | UC-12 `total_redeem_count`, UC-03 `item_view_count` |
+| `WINDOW` | Counter reset theo kỳ cố định | UC-03 `weekly_view_count` |
+| `STREAK` | Đếm chuỗi liên tiếp theo ngày/tuần/tháng | UC-02 `login_streak` |
+
+Quy tắc:
+
+- `period` chỉ bắt buộc với `WINDOW` và `STREAK`.
+- `STREAK DAILY` bỏ qua event trùng ngày để login nhiều lần không tăng streak.
+- `WINDOW WEEKLY` tính theo ISO week, thứ Hai là ngày đầu tuần.
+- Nếu khác period/window, value reset về `val`, không reset về 0 rồi increment riêng.
+
+#### 6.4. `APPEND_UNIQUE`
+
+Thêm phần tử vào list, có chống trùng và giới hạn độ dài.
+
+| Thuộc tính | Giá trị |
+|:---|:---|
+| Kiểu value | Array |
+| Options | `maxSize`, `dedupeKey` |
+| Use cases | UC-03 `recently_viewed` |
+
+Ví dụ:
+
+```json
+{
+ "op": "APPEND_UNIQUE",
+ "entity": "Customer",
+ "entityId": "${actor.customerId}",
+ "key": "recently_viewed",
+ "val": {
+ "itemId": "${entities.itemId}",
+ "viewedAt": "${occurredAt}"
+ },
+ "options": {
+ "dedupeKey": "itemId",
+ "maxSize": 50
+ }
+}
+```
+
+#### 6.5. Không tạo operation riêng ở phase 1
+
+Các operation sau **chưa cần tách riêng** vì 13 use case hiện tại xử lý được bằng 4 primitive trên:
+
+- `STREAK_INCREMENT`: dùng `INCREMENT` với `mode = STREAK`.
+- `CHECK_AND_UPDATE_TIMER`: dùng `SET` với threshold `evaluateOn = BEFORE_UPDATE`.
+- `APPEND`: dùng `APPEND_UNIQUE`; nếu cần cho phép trùng sau này, thêm option `unique = false` thay vì tạo op mới.
+- `COMPUTE_GAP`: dùng threshold operator `TIME_GAP_GT` trên old value và `occurredAt`.
+
+---
+
+### 7. Threshold & Trigger Policy
+
+#### 7.1. Threshold Format
+
+```json
+{
+ "thresholdId": "View_10_Items_Weekly",
+ "evaluateOn": "AFTER_UPDATE",
+ "operator": ">=",
+ "left": "CURRENT_VALUE",
+ "target": 10,
+ "triggerEvent": "View_10_Items_Weekly",
+ "triggerPolicy": "ONCE_PER_PERIOD",
+ "period": "WEEKLY"
+}
+```
+
+| Field | Bắt buộc | Ghi chú |
+|:---|:---:|:---|
+| `thresholdId` | Yes | Stable id, không đổi khi đổi display name |
+| `evaluateOn` | Yes | `AFTER_UPDATE` cho counter/state thông thường; `BEFORE_UPDATE` cho check giá trị cũ trước khi ghi |
+| `operator` | Yes | `==`, `>=`, `>`, `TIME_GAP_GT` |
+| `left` | Theo operator | `CURRENT_VALUE` hoặc `OLD_VALUE`; UI có thể ẩn field này và tự set theo `evaluateOn` |
+| `right` | Theo operator | Giá trị so sánh bên phải, ví dụ `${occurredAt}` cho `TIME_GAP_GT` |
+| `target` | Yes | Giá trị so sánh |
+| `triggerEvent` | Yes | Tên Generated Direct Event emit vào `events-topic` |
+| `triggerPolicy` | Yes | `ONCE_LIFETIME` hoặc `ONCE_PER_PERIOD` |
+| `period` | Khi cần | Bắt buộc với `ONCE_PER_PERIOD` |
+
+`evaluateOn` giúp UI chỉ cần một Threshold Builder duy nhất:
+
+- Mặc định là `AFTER_UPDATE`, dùng cho `INCREMENT`, `SET_TRUE_ONCE` và phần lớn threshold.
+- `BEFORE_UPDATE` chỉ nên hiển thị khi operation là `SET` và attribute type là `DateTime`.
+- Với `TIME_GAP_GT`, UI có thể hiển thị đơn giản: "Giá trị cũ của `last_login_date` cách `occurredAt` lớn hơn 90 ngày", không cần bắt user chọn `left/right`.
+
+#### 7.2. Trigger Policy
+
+| Policy | Ý nghĩa | Ví dụ |
+|:---|:---|:---|
+| `ONCE_LIFETIME` | Một entity chỉ trigger một lần trong toàn bộ vòng đời | First login, survey completed, welcome offer |
+| `ONCE_PER_PERIOD` | Một entity chỉ trigger một lần trong mỗi kỳ | Weekly view reward, yearly inactive account reward |
+
+`threshold_fire_log` cần có đủ scope để enforce đúng policy.
+
+```text
+Unique key for ONCE_LIFETIME:
+ entity_type + entity_id + attribute_key + threshold_id
+
+Unique key for ONCE_PER_PERIOD:
+ entity_type + entity_id + attribute_key + threshold_id + period_type + period_key
+```
+
+Ví dụ `period_key`:
+
+- `WEEKLY`: `2026-W29`
+- `MONTHLY`: `2026-07`
+- `YEARLY`: `2026`
+
+#### 7.3. Threshold Fire Log
+
+```text
+threshold_fire_log
+- id
+- entity_type
+- entity_id
+- attribute_key
+- threshold_id
+- trigger_policy
+- period_type
+- period_key
+- source_event_id
+- trigger_event_id
+- fired_at
+- created_at
+```
+
+Quy trình:
+
+1. Operation thực thi xong và value thật sự thay đổi.
+2. Attribute Service evaluate threshold.
+3. Nếu đạt, insert `threshold_fire_log` bằng unique key theo policy.
+4. Nếu insert thành công, tạo Generated Direct Event và insert vào outbox.
+5. Nếu unique conflict, bỏ qua vì threshold đã fire trong scope đó.
+
+#### 7.4. Generated Direct Event Payload
+
+Khi threshold đạt, Attribute không publish lại Tracking Event gốc. Nó tạo một Direct Event mới, có semantic rõ ràng cho TP.
+
+```json
+{
+ "eventId": "evt-direct-20260716-001",
+ "eventName": "Login_Streak_7_Days",
+ "eventCategory": "DIRECT",
+ "schemaVersion": 1,
+ "source": "attribute-service",
+ "occurredAt": "2026-07-16T10:30:00+07:00",
+ "publishedAt": "2026-07-16T10:30:05+07:00",
+ "correlationId": "corr-789",
+ "idempotencyKey": "reward:Login_Streak_7_Days:Customer:CUST-001",
+ "causedByEventId": "evt-login-20260716-abc123",
+ "actor": {
+ "customerId": "CUST-001"
+ },
+ "entities": {
+ "attributeKey": "login_streak"
+ },
+ "payload": {
+ "thresholdId": "Login_Streak_7_Days",
+ "attributeValue": 7,
+ "triggerPolicy": "ONCE_LIFETIME"
+ }
+}
+```
+
+TP chỉ dựa vào `eventName`/transaction code/rule config để quyết định reward. Attribute không truyền số điểm/voucher cần cấp, trừ khi đó là metadata để audit.
+
+---
+
+### 8. Partitioning, Concurrency & Idempotency
+
+#### 8.1. Partition Key
+
+Không partition cố định theo `customerId`. Partition key phải theo state được update.
+
+```text
+partitionKey = entityType + ":" + entityId
+```
+
+Ví dụ:
+
+| Event | State update | Partition key |
+|:---|:---|:---|
+| `Login` | Customer streak/cờ login | `Customer:CUST-001` |
+| `View_Item` weekly count | Customer weekly view | `Customer:CUST-001` |
+| `View_Item` item counter | Item view count | `Item:ITEM-789` |
+| `Item_Redeemed_Count` | Item redeem count | `Item:ITEM-789` |
+| `Survey_Completed` | Customer survey flag | `Customer:CUST-001` |
+
+Nếu một incoming event cần update nhiều entity khác nhau, Event Processor tách thành nhiều tracking messages, mỗi message có partition key riêng. Ví dụ `View_Item` có thể tạo:
+
+- Message 1: update `Customer:CUST-001` cho `weekly_view_count` và `recently_viewed`.
+- Message 2: update `Item:ITEM-789` cho `item_view_count`.
+
+#### 8.2. Idempotency
+
+- Mỗi service lưu processed event theo `eventId` hoặc `idempotencyKey`.
+- TP dùng business idempotency key cho transaction/reward để chống cộng điểm/cấp voucher trùng.
+- Attribute dùng event idempotency để tránh tăng counter trùng khi producer retry.
+- Notification Service dùng `eventId` hoặc notification dedupe key để tránh gửi thông báo trùng.
+
+---
+
+### 9. Use Case Mapping
+
+| UC | Event/Command | Category | Flow | Operation | Trigger/Output |
+|:---|:---|:---|:---|:---|:---|
+| UC-01 First Login | `Login` | Tracking Event -> Generated Direct Event | EP -> `tracking-topic` -> ATTR -> `events-topic` -> TP | `SET_TRUE_ONCE first_login_flag` | `First_Login_Reward` Direct Event -> +100đ + noti |
+| UC-02 Daily Login | `Login` | Tracking Event -> Generated Direct Event | EP -> `tracking-topic` -> ATTR -> `events-topic` -> TP | `INCREMENT login_streak mode=STREAK period=DAILY` | `Login_Streak_7_Days`, `Login_Streak_30_Days` Direct Events |
+| UC-03 View Item | `View_Item` | Tracking Event, optional Generated Direct Event | EP -> `tracking-topic` -> ATTR | `INCREMENT weekly_view_count mode=WINDOW period=WEEKLY`, `INCREMENT item_view_count mode=LIFETIME`, `APPEND_UNIQUE recently_viewed` | Weekly threshold có thể emit `View_10_Items_Weekly` Direct Event; item/recent list phục vụ recommend |
+| UC-04 Transfer Balance | `Transfer_Balance` command | Direct Flow | App -> TP sync API | N/A | Debit/Credit ACID + noti |
+| UC-05 Inactive Account Login | `Login` | Tracking Event -> Generated Direct Event | EP -> `tracking-topic` -> ATTR -> `events-topic` -> TP | `SET last_login_date` with threshold `evaluateOn=BEFORE_UPDATE`, `TIME_GAP_GT 90D` | `Account_Reactivated` Direct Event -> welcome back offer/noti |
+| UC-06 Welcome Offer | `Welcome_Offer` | Direct Event | External/Card System -> EP -> `events-topic` -> TP | N/A | +100đ, `ONCE_LIFETIME` by TP idempotency |
+| UC-07 Update Profile | `Profile_Completed` | Direct Event | Profile Service -> EP -> `events-topic` -> TP | N/A | +100đ nếu hoàn tất trước deadline |
+| UC-08 Survey | `Survey_Completed` | Tracking Event -> Generated Direct Event | EP -> `tracking-topic` -> ATTR -> `events-topic` -> TP | `SET_TRUE_ONCE survey_completed:{surveyId}` | `Survey_Completed_Reward` Direct Event; chỉ reward khi set flag thành công |
+| UC-09 Redeem Item | `Redeem_Item` command | Direct Flow | App -> TP sync API | N/A | Trừ điểm + reserve inventory ACID; TP publish noti và tracking event sau commit |
+| UC-10 Merchant Scan | `Scan_Voucher` command | Direct Flow | Merchant App -> TP sync API | N/A | Voucher USED ACID + noti |
+| UC-11 Password Expiring | `Password_Expiring` | Notification Event | Marketing -> EP/`noti-topic` -> NS | N/A | Push/Email cảnh báo |
+| UC-12 Most Redeem Item | `Item_Redeemed_Count` | Tracking Event | TP outbox -> `tracking-topic` -> ATTR | `INCREMENT total_redeem_count mode=LIFETIME` on Item | Cập nhật ranking/reporting |
+| UC-13 Dormant User | `Dormant_User` | Direct Event | Marketing -> EP -> `events-topic` -> TP | N/A | Cấp ưu đãi riêng + noti |
+
+---
+
+### 10. Chi tiết Các Use Case Quan trọng
+
+#### 10.1. UC-08 Survey Reward Gate
+
+UC-08 không nên dual publish trực tiếp tới cả TP và Attribute, vì có nguy cơ cấp voucher trước khi biết survey flag đã được set thành công.
+
+Luồng chuẩn:
+
+```mermaid
+sequenceDiagram
+ participant APP as Member App
+ participant EP as Event Processor
+ participant TT as tracking-topic
+ participant ATTR as Attribute Service
+ participant ET as events-topic
+ participant TP as Transaction Service
+
+ APP->>EP: Survey_Completed
+ EP->>TT: tracking message
+ TT->>ATTR: Consume
+ ATTR->>ATTR: SET_TRUE_ONCE survey_completed:{surveyId}
+ alt first completion
+ ATTR->>ET: Generated Direct Event: Survey_Completed_Reward
+ ET->>TP: Consume Direct Event and grant voucher
+ else already completed
+ ATTR->>ATTR: No-op, no reward
+ end
+```
+
+#### 10.2. UC-12 Redeem Counter
+
+Redeem là Direct Flow, không đi qua Event Processor cho primary transaction.
+
+Luồng chuẩn:
+
+1. Member App gọi TP API `Redeem_Item`.
+2. TP validate balance, inventory, voucher rule trong DB transaction.
+3. TP commit thành công.
+4. TP outbox publish:
+ - Notification event vào `noti-topic`.
+ - `Item_Redeemed_Count` vào `tracking-topic`.
+5. Attribute tăng `Item:{itemId}:total_redeem_count`.
+
+Nếu redeem fail, không publish tracking event.
+
+---
+
+### 11. Data Model Khuyến nghị
+
+#### 11.1. Attribute Record
+
+```text
+attribute_record
+- entity_type -- Customer, Item, Voucher...
+- entity_id
+- attribute_key
+- value_json
+- value_type
+- last_event_id
+- last_event_time
+- created_at
+- updated_at
+
+unique(entity_type, entity_id, attribute_key)
+```
+
+#### 11.2. Processed Event
+
+```text
+processed_event
+- service_name
+- event_id
+- idempotency_key
+- processed_at
+- status
+
+unique(service_name, event_id)
+unique(service_name, idempotency_key) where idempotency_key is not null
+```
+
+#### 11.3. Outbox
+
+```text
+outbox_event
+- id
+- aggregate_type
+- aggregate_id
+- event_name
+- event_payload
+- target_topic
+- idempotency_key
+- status
+- retry_count
+- next_retry_at
+- created_at
+- published_at
+```
+
+---
+
+### 12. Reliability, DLQ & Replay
+
+- Consumer retry có giới hạn, ví dụ 3 lần nhanh + backoff.
+- Sau khi retry hết, event vào DLQ tương ứng: `events-dlq`, `tracking-dlq`, `noti-dlq`.
+- DLQ payload phải giữ nguyên envelope, error code, stack trace tóm tắt, service name và failed timestamp.
+- Replay DLQ phải đi qua cùng idempotency guard như event bình thường.
+- Monitoring tối thiểu:
+ - consumer lag theo topic/consumer group;
+ - outbox pending count;
+ - DLQ rate;
+ - reward duplicate/conflict count;
+ - notification send failure rate.
+
+---
+
+### 13. Security & Governance
+
+- Client không được gửi instruction/rule trong event. Instruction chỉ lấy từ registry server-side.
+- Event Processor validate schema, source, authentication/authorization và schema version.
+- TP không tin tuyệt đối Generated Direct Event; TP vẫn check transaction code, campaign rule, eligibility và idempotency trước khi cấp reward.
+- Các field nhạy cảm trong event payload cần được mask/encrypt theo chuẩn bảo mật hiện hành.
+- Registry thay đổi phải có audit log: ai sửa, sửa gì, effective time, rollback version.
+
+---
+
+### 14. Kết luận Thiết kế
+
+Thiết kế v2 chốt các điểm chính:
+
+- `Direct Flow` là sync API vào TP cho nghiệp vụ cần phản hồi ngay và ACID.
+- `Direct Event` là async Kafka event vào `events-topic` cho reward/rule không cần sync response.
+- Tracking/counter/state nằm ở Attribute, không đẩy counter tần suất cao vào TP.
+- Khi Tracking Event đạt threshold, Attribute sinh một Generated Direct Event mới vào `events-topic`; TP consume event này như Direct Event async bình thường.
+- UC-08 dùng Attribute làm gate để tránh cấp voucher trùng.
+- UC-12 lấy event sau transaction thành công từ TP outbox, không làm ảnh hưởng luồng redeem sync.
+- Operation catalog phase 1 chỉ gồm `SET`, `SET_TRUE_ONCE`, `INCREMENT`, `APPEND_UNIQUE`; các biến thể streak/window/timer là mode hoặc threshold, không phải operation riêng.
+- Trigger policy chỉ cần `ONCE_LIFETIME` và `ONCE_PER_PERIOD` cho giai đoạn hiện tại.
diff --git a/specs/archive/Event_Driven_Architecture_Proposal.md b/specs/archive/Event_Driven_Architecture_Proposal.md
new file mode 100644
index 0000000..78994d9
--- /dev/null
+++ b/specs/archive/Event_Driven_Architecture_Proposal.md
@@ -0,0 +1,392 @@
+# Event Driven Architecture Proposal
+
+## 1. Mục tiêu
+
+- **Chuẩn hóa xử lý Event:** Tạo ra một flow chung duy nhất cho mọi luồng sự kiện từ các Client/App gửi về hệ thống.
+- **Giảm tải Transaction Service (TP):** Bảo vệ core engine (TP) khỏi hàng triệu sự kiện "rác" (như Login, View, Click). TP chỉ thức dậy khi có giao dịch tài chính hoặc cần phát thưởng thực sự.
+- **Cho phép Marketing cấu hình Campaign linh hoạt:** Cung cấp bộ công cụ (thông qua Attribute/Event Processor) để cấu hình các ngưỡng thưởng (threshold) đa dạng mà không cần Dev phải sửa code.
+- **Đảm bảo giao dịch tài chính ACID:** Các giao dịch cộng/trừ điểm, phát voucher, trừ kho vẫn được xử lý tập trung tại Transaction Service với độ an toàn dữ liệu cao nhất.
+
+## 2. Nguyên tắc thiết kế
+
+1. **Transaction Service** CHỈ xử lý các nghiệp vụ tài chính, Rule Engine (cộng/trừ điểm, tặng voucher, giới hạn thưởng). Không giữ các bộ đếm rác.
+2. **Attribute Service** (Dumb Executor) CHỈ làm nhiệm vụ lưu trữ trạng thái (state), tính toán các phép toán cơ bản (cộng dồn, đếm ngày, kiểm tra khoảng thời gian) dựa trên chỉ thị. Hoàn toàn KHÔNG biết về business logic (như thưởng bao nhiêu, rule gì).
+3. **Event Processor** (Smart Gateway) CHỈ làm nhiệm vụ Validate (kiểm tra định dạng) và Routing (phân loại & điều phối luồng sự kiện). Đóng vai trò là nơi lưu trữ cấu hình (Event Registry).
+4. **Marketing Service** CHỈ làm nhiệm vụ lập lịch (Scheduler) hoặc định kỳ quét tập khách hàng để phát sinh Event đầu vào. KHÔNG trực tiếp thực hiện cộng điểm hay xử lý tài chính.
+
+## 3. Kiến trúc tổng thể
+
+Mô hình di chuyển dữ liệu xuyên suốt hệ thống theo một chiều (Unidirectional Data Flow):
+
+```text
+Client (App/Web) / Scheduler
+ │
+ ▼
+[ Event Processor ] ──(Phân loại & Đóng gói)
+ │
+ ▼
+ [ Kafka ] <────────────────────────────────┐
+ │ │ (Publish Trigger Event khi đạt Threshold)
+ ├──> (tracking-topic) ──> [ Attribute Service ]
+ │
+ ├──> (events-topic) ──> [ Transaction Service ]
+ │ │
+ └──> (noti-topic) ──> [ Notification Service ]
+```
+
+## 4. Phân loại Event
+
+Mọi sự kiện đầu vào được chia làm 3 loại duy nhất:
+
+- **Direct Event:** Cần xử lý logic/tài chính ngay lập tức (VD: Redeem Item, Transfer Balance, Welcome Offer). Sẽ được đẩy thẳng cho Transaction Service.
+- **Tracking Event:** Cần tích lũy, đếm hoặc kiểm tra điều kiện trước khi trigger (VD: Daily Login, View Item). Sẽ được đẩy cho Attribute Service để lưu trữ và tính toán.
+- **Notification Event:** Các sự kiện chỉ mang mục đích gửi thông báo, không có logic tài chính hay đếm (VD: Cảnh báo mật khẩu sắp hết hạn). Sẽ được đẩy thẳng cho Notification Service.
+
+## 5. Vai trò các Service
+
+- **Event Processor:** `Validate + Route`. Nơi nhận Thin Event, tra cứu cấu hình, biến thành Fat Payload và định tuyến vào đúng Topic Kafka.
+- **Attribute:** `Tracking / State`. Cỗ máy tính toán cơ bắp. Nhận Fat Payload, thực thi các phép toán (+, -, gán cờ) vào DB. Sinh sự kiện mới nếu đạt threshold.
+- **Transaction:** `Rule Engine + Reward`. Đảm bảo tính ACID của giao dịch. Cộng điểm, trừ điểm, trả voucher theo đúng Rule được định cấu hình.
+- **Notification:** `Push`. Gửi SMS, Email, Firebase Push tới End-User.
+- **Marketing:** `Scheduler`. Quét tập dữ liệu định kỳ (cronjob) để sinh ra sự kiện tự động (như User ngủ đông, sắp sinh nhật, mật khẩu hết hạn).
+
+## 6. Standard Flow (Luồng tiêu chuẩn)
+
+### Tracking Flow (Dành cho các sự kiện tích lũy)
+Áp dụng cho các tính năng như: Điểm danh hàng ngày, Chuỗi đăng nhập, Xem Item 10 lần.
+`Client` ──> `Event Processor` ──> `tracking-topic` ──> `Attribute Service` ──(Đạt ngưỡng/Threshold)──> `events-topic` ──> `Transaction Service` ──> `Notification`
+
+### Direct Flow (Dành cho các sự kiện tài chính/tức thời)
+Áp dụng cho các tính năng: Đổi quà (Redeem), Chuyển điểm (Transfer), Hoàn tất hồ sơ.
+`Client / Service khác` ──> `Event Processor` ──> `events-topic` ──> `Transaction Service` ──> `Notification`
+
+## 7. Use Case Template
+
+Để thống nhất ngôn ngữ giao tiếp, mọi Use Case mới sẽ được mô tả theo Template chuẩn sau:
+
+- **Business Requirement:** Yêu cầu nghiệp vụ (Mô tả ngắn gọn mục đích).
+- **Event:** Tên sự kiện gốc từ Client gửi lên (VD: `Login`, `ViewItem`).
+- **Event Type:** Phân loại (`Direct`, `Tracking`, `Notification`).
+- **Processing Flow:** Luồng xử lý (`Direct Flow` hoặc `Tracking Flow`).
+- **Attributes:** Các thuộc tính cần cập nhật lưu tại Attribute Service (VD: `login_streak`, `weekly_view_count`). Nếu là Direct Flow thì để trống.
+- **Trigger:** Tên sự kiện sinh ra khi đủ điều kiện để báo cho Transaction Service (VD: `Login_Streak_7_Days`).
+- **Output:** Kết quả cuối cùng (VD: +100 điểm, tặng 1 voucher).
+
+## 8. Operation Catalog
+
+Attribute Service hỗ trợ một tập cố định các **Primitive Operations** (Toán tử nguyên thủy). Marketing/BA có thể lắp ghép các toán tử này để tạo ra vô số Campaign mà không cần Dev sửa code.
+
+### 8.1. Quy tắc chung
+
+- **Timezone:** Mọi phép so sánh ngày/giờ đều được convert về `Asia/Ho_Chi_Minh` trước khi xử lý.
+- **Time Source:** Luôn dùng **Event Time** (thời điểm sự kiện xảy ra tại Client/Source, nằm trong payload) — KHÔNG dùng Processing Time (thời điểm Attribute Service nhận event). Điều này đảm bảo kết quả deterministic, không phụ thuộc vào tải hệ thống hay Kafka lag.
+- **Key chưa tồn tại:** Khi nhận Instruction cho một Key chưa có trong DB, Attribute Service tự khởi tạo giá trị mặc định rồi thực thi Operation.
+- **Concurrency:** Kafka topic `tracking-topic` PHẢI partition theo `customerId`. Mọi event của cùng một user luôn vào cùng một partition → được xử lý tuần tự bởi một consumer duy nhất → không xảy ra race condition.
+- **Threshold Trigger:** Mặc định `triggerOnce: true`. Mỗi threshold chỉ fire đúng 1 lần khi value lần đầu thỏa mãn condition. Attribute Service lưu flag `threshold_fired` để ngăn trigger lặp lại.
+
+### 8.2. Attribute Record
+
+Mỗi Key trong Attribute Service được lưu trữ với cấu trúc sau:
+
+```text
+┌─ Attribute Record ──────────────────────────────────┐
+│ key: "user:CUST-001:login_streak" │
+│ value: 5 (Number/String/Bool) │
+│ last_updated: "2026-07-15T23:50:00+07:00" │
+│ created_at: "2026-07-10T08:00:00+07:00" │
+│ threshold_fired: false │
+└──────────────────────────────────────────────────────┘
+```
+
+- `last_updated` được Attribute Service tự cập nhật (bằng Event Time) mỗi khi có write thành công. Đây là nền tảng để xử lý mọi bài toán thời gian.
+- `threshold_fired` được set `true` khi threshold trigger lần đầu (chế độ `triggerOnce`).
+
+### 8.3. Phase 1 — Operations cần thiết cho 13 Use Cases hiện tại
+
+#### `SET`
+
+Ghi đè giá trị vô điều kiện (Overwrite).
+
+| Thuộc tính | Giá trị |
+|:---|:---|
+| Kiểu value | `String`, `Number`, `Boolean`, `Date` |
+| Key chưa tồn tại | Tạo mới với value được truyền vào |
+| Idempotent | Không. Mỗi lần gọi đều ghi đè |
+
+**Use Case:** UC-05 — Mỗi lần User login, cập nhật `SET last_login_date = eventTime`. Kết hợp với Threshold `TIME_GAP_GT` để phát hiện khoảng cách ngủ đông.
+
+**Hành vi chi tiết:**
+1. Nếu có threshold `TIME_GAP_GT`: Kiểm tra threshold **TRƯỚC** khi ghi đè. So sánh `(eventTime - old_value)`. Nếu thỏa mãn → sinh Trigger Event. Sau đó mới ghi đè value mới.
+2. Nếu không có threshold: Ghi đè trực tiếp.
+
+```text
+Ví dụ UC-05 (Login sau 90 ngày ngủ đông):
+
+ Trạng thái hiện tại: last_login_date = "2026-04-15"
+ Event đến: eventTime = "2026-07-16"
+
+ Bước 1: Check threshold TIME_GAP_GT 90 days
+ → (2026-07-16) - (2026-04-15) = 92 ngày > 90 ✓
+ → Publish Trigger Event: "Account_Reactivated"
+ Bước 2: SET last_login_date = "2026-07-16"
+```
+
+#### `SET_TRUE_ONCE`
+
+Đánh dấu cờ Boolean = `true`. Chỉ có hiệu lực **đúng 1 lần duy nhất** khi key chưa tồn tại hoặc đang là `false`/`null`. Đảm bảo **Idempotency** (tính lũy đẳng) tuyệt đối.
+
+| Thuộc tính | Giá trị |
+|:---|:---|
+| Kiểu value | `Boolean` (cố định) |
+| Key chưa tồn tại | Tạo mới = `true`, sinh Trigger Event |
+| Key đã là `true` | **Bỏ qua hoàn toàn** — không ghi, không trigger |
+
+**Use Cases:** UC-01 (First Login), UC-08 (Survey Completed).
+
+**Hành vi chi tiết:**
+```text
+Ví dụ UC-01 (First Login):
+
+ Lần 1: key "first_login" chưa tồn tại
+ → Tạo mới: value = true
+ → Trigger Event: "First_Login" ✓
+
+ Lần 2: key "first_login" = true (đã tồn tại)
+ → Bỏ qua. Không ghi, không trigger.
+ → User KHÔNG nhận thưởng lần 2. An toàn.
+```
+
+#### `INCREMENT`
+
+Cộng dồn giá trị số. Hỗ trợ 2 chế độ thời gian tùy chọn: **Streak** (chuỗi liên tiếp) và **Window** (cửa sổ chu kỳ).
+
+| Thuộc tính | Giá trị |
+|:---|:---|
+| Kiểu value | `Number` |
+| Key chưa tồn tại | Tạo mới = `0`, rồi thực thi `+val` → kết quả = `val` |
+| Tham số bắt buộc | `val` (số cần cộng, thường = 1) |
+| Tham số tùy chọn | `streak` hoặc `window` (loại trừ nhau) |
+
+**Chế độ 1: Không có streak/window** — Cộng dồn thuần túy, không reset.
+
+```text
+Ví dụ UC-12 (Most Redeem — Bộ đếm tích lũy vĩnh viễn):
+
+ INCREMENT total_redeem_count, val: 1
+
+ Lần 1: 0 + 1 = 1
+ Lần 2: 1 + 1 = 2
+ ...
+ Lần N: (N-1) + 1 = N
+
+ Không bao giờ reset. Dùng để thống kê, ranking.
+```
+
+**Chế độ 2: `streak` — Đếm chuỗi liên tiếp.**
+
+Dùng để đếm số ngày/tuần/tháng liên tiếp mà user thực hiện hành vi. Nếu **đứt chuỗi** (bỏ qua 1 chu kỳ), reset về 1 và đếm lại.
+
+Giá trị `streak` hợp lệ: `DAILY`, `WEEKLY`, `MONTHLY`.
+
+Logic xử lý (dựa trên `last_updated` và `eventTime`, đã convert về `Asia/Ho_Chi_Minh`):
+
+```text
+streak: "DAILY"
+
+ Tính khoảng cách ngày giữa eventTime và last_updated:
+
+ ┌──────────────────────┬──────────────────────────────────────────┐
+ │ Khoảng cách │ Hành vi │
+ ├──────────────────────┼──────────────────────────────────────────┤
+ │ Cùng ngày │ Bỏ qua (Idempotent — login 5 lần/ngày │
+ │ │ chỉ đếm 1) │
+ │ Đúng 1 ngày trước │ value = value + val (streak tiếp tục) │
+ │ Cách > 1 ngày │ value = 1 (gán thẳng — streak đứt, │
+ │ │ bắt đầu chuỗi mới từ hôm nay) │
+ │ Key chưa tồn tại │ value = 1 (bắt đầu chuỗi mới) │
+ └──────────────────────┴──────────────────────────────────────────┘
+```
+
+> **Lưu ý quan trọng:** Khi streak đứt, giá trị được gán thẳng = 1 trong **một thao tác atomic duy nhất** (không phải gọi RESET rồi INCREMENT riêng). Vì hôm nay user CÓ thực hiện hành vi, nên streak phải là 1 chứ không phải 0.
+
+```text
+Ví dụ UC-02 (Daily Login Streak):
+
+ INCREMENT login_streak, val: 1, streak: "DAILY"
+ Threshold: { "==": 7, triggerEvent: "Login_Streak_7_Days" }
+
+ Ngày 1 (Thứ Hai): key mới → value = 1
+ Ngày 2 (Thứ Ba): hôm qua → value = 2
+ Ngày 3 (Thứ Tư): hôm qua → value = 3
+ Ngày 3 (Thứ Tư, login lần 2): cùng ngày → BỎ QUA
+ Ngày 4 (Thứ Năm): hôm qua → value = 4
+ --- User không login Thứ Sáu ---
+ Ngày 6 (Thứ Bảy): cách 2 ngày → value = 1 (streak đứt, đếm lại)
+ Ngày 7 (Chủ Nhật): hôm qua → value = 2
+ ...
+ Ngày N (liên tiếp 7): hôm qua → value = 7 → Trigger! ✓
+```
+
+**Chế độ 3: `window` — Đếm gộp trong chu kỳ cố định.**
+
+Dùng để đếm tổng số lần thực hiện hành vi trong một khoảng thời gian. Hết chu kỳ thì reset, bắt đầu đếm lại từ đầu. **Không quan tâm liên tiếp hay không.**
+
+Giá trị `window` hợp lệ: `DAILY`, `WEEKLY`, `MONTHLY`.
+
+```text
+window: "WEEKLY"
+
+ Tính xem eventTime và last_updated có cùng chu kỳ không:
+
+ ┌──────────────────────┬──────────────────────────────────────────┐
+ │ Điều kiện │ Hành vi │
+ ├──────────────────────┼──────────────────────────────────────────┤
+ │ Cùng tuần (ISO 8601) │ value = value + val (tiếp tục đếm) │
+ │ Khác tuần │ value = 1 (reset, bắt đầu chu kỳ mới) │
+ │ Key chưa tồn tại │ value = 1 (bắt đầu chu kỳ mới) │
+ └──────────────────────┴──────────────────────────────────────────┘
+```
+
+> **Quy ước chu kỳ:** `WEEKLY` tính theo ISO 8601 (Thứ Hai là ngày đầu tuần). `MONTHLY` tính theo tháng dương lịch (ngày 1 là ngày đầu tháng).
+
+```text
+Ví dụ UC-03 (View Item — 10 lần xem trong tuần):
+
+ INCREMENT weekly_view_count, val: 1, window: "WEEKLY"
+ Threshold: { "==": 10, triggerEvent: "View_10_Items_Weekly" }
+
+ Thứ Hai: xem 3 item → value = 3
+ Thứ Tư: xem 4 item → value = 7
+ Thứ Sáu: xem 3 item → value = 10 → Trigger! ✓
+ --- Sang tuần mới (Thứ Hai tiếp theo) ---
+ Thứ Hai: xem 1 item → value = 1 (reset, đếm lại từ đầu)
+```
+
+#### `APPEND`
+
+Thêm phần tử vào cuối danh sách (List/Array).
+
+| Thuộc tính | Giá trị |
+|:---|:---|
+| Kiểu value | `Array` |
+| Key chưa tồn tại | Tạo mới = `[]`, rồi thêm phần tử |
+| Tham số tùy chọn | `unique` (Boolean), `maxSize` (Number) |
+
+**Options:**
+- `unique: true` — Chỉ thêm nếu phần tử chưa có trong danh sách. Bỏ qua nếu trùng.
+- `maxSize: N` — Giới hạn kích thước tối đa. Khi đạt `maxSize`, xóa phần tử **cũ nhất** (FIFO) để nhường chỗ cho phần tử mới.
+
+```text
+Ví dụ UC-03 (Lưu vết sản phẩm đã xem):
+
+ APPEND viewed_items, val: "item_789", unique: true, maxSize: 50
+
+ Bước 1: viewed_items = ["item_123", "item_456"]
+ → Thêm "item_789"
+ → viewed_items = ["item_123", "item_456", "item_789"]
+
+ Trùng lặp: APPEND viewed_items, val: "item_123", unique: true
+ → "item_123" đã có → BỎ QUA
+
+ Tràn maxSize: list có 50 items, thêm "item_new"
+ → Xóa phần tử cũ nhất (FIFO): "item_001"
+ → Thêm "item_new" vào cuối
+ → list vẫn = 50 items
+```
+
+### 8.4. Phase 2 — Operations mở rộng (khi có Campaign mới)
+
+Các operations sau đây đã được định nghĩa sẵn nhưng **chưa có Use Case cụ thể** trong 13 UC hiện tại. Sẽ implement khi phát sinh nhu cầu.
+
+| Operation | Mô tả | Ví dụ Use Case tiềm năng |
+|:---|:---|:---|
+| `SET_IF_NULL` | Ghi giá trị CHỈ khi key chưa tồn tại. Khác `SET_TRUE_ONCE` ở chỗ lưu được giá trị bất kỳ (không chỉ Boolean). | Lưu `first_transaction_date` để tặng thưởng user giao dịch lần đầu trong tháng 7. |
+| `DECREMENT` | Trừ giá trị số (`value = value - val`). | Quản lý hạn mức Campaign (quota). Lưu ý: Nếu liên quan tài chính, quota nên nằm ở Transaction Service. |
+| `RESET` | Đưa value về trạng thái mặc định (`0`, `null`, `[]`). | Reset thủ công bộ đếm từ một Admin Event hoặc Cronjob. Phase 1 đã có auto-reset qua `streak` và `window`. |
+| `MAX` | So sánh và chỉ ghi đè nếu giá trị mới **lớn hơn** giá trị hiện tại. | "Tặng quà cho hóa đơn đơn lẻ lớn nhất đạt 5 triệu": `MAX single_receipt_value`. |
+| `MIN` | So sánh và chỉ ghi đè nếu giá trị mới **nhỏ hơn** giá trị hiện tại. | Tìm giá trị giao dịch nhỏ nhất để phân loại micro-transaction. |
+
+### 8.5. Threshold (Ngưỡng kích hoạt)
+
+Threshold là điều kiện để Attribute Service quyết định có publish Trigger Event hay không. Mỗi Instruction có thể đính kèm **tối đa 1 threshold**.
+
+**Operators hỗ trợ:**
+
+| Operator | Ý nghĩa | Ví dụ |
+|:---|:---|:---|
+| `==` | Bằng | `login_streak == 7` |
+| `>=` | Lớn hơn hoặc bằng | `total_spend >= 5000000` |
+| `>` | Lớn hơn | `view_count > 10` |
+| `TIME_GAP_GT` | Khoảng cách thời gian lớn hơn N ngày (so sánh `eventTime - old_value`) | `last_login_date` cách `eventTime` > 90 ngày |
+
+**Quy tắc trigger:**
+- Mặc định `triggerOnce: true`: Threshold chỉ fire **1 lần duy nhất** khi value lần đầu thỏa mãn condition. Attribute Service đánh dấu `threshold_fired = true` trên record. Các lần thỏa mãn tiếp theo sẽ bị bỏ qua.
+- Threshold chỉ được evaluate khi **value thực sự thay đổi**. Nếu operation bị bỏ qua (VD: `SET_TRUE_ONCE` khi đã true, hoặc `INCREMENT` với `streak` cùng ngày), threshold không được evaluate.
+
+## 9. Instruction Format (Định dạng Chỉ thị)
+
+Mỗi Instruction trong Fat Payload tuân theo cấu trúc chuẩn sau:
+
+```json
+{
+ "op": "INCREMENT",
+ "key": "login_streak",
+ "val": 1,
+ "streak": "DAILY",
+ "options": {},
+ "threshold": {
+ "condition": "==",
+ "target": 7,
+ "triggerOnce": true,
+ "triggerEvent": "Login_Streak_7_Days"
+ }
+}
+```
+
+| Field | Bắt buộc | Mô tả |
+|:---|:---|:---|
+| `op` | ✓ | Tên Operation: `SET`, `SET_TRUE_ONCE`, `INCREMENT`, `APPEND` |
+| `key` | ✓ | Tên attribute key (không bao gồm prefix `customerId`, prefix được tự động thêm bởi Attribute Service) |
+| `val` | ✓ | Giá trị truyền vào (Number, String, Boolean tùy `op`) |
+| `streak` | | Chế độ đếm chuỗi liên tiếp. Giá trị: `DAILY`, `WEEKLY`, `MONTHLY`. Chỉ dùng với `INCREMENT`. **Loại trừ với `window`** |
+| `window` | | Chế độ đếm gộp theo chu kỳ. Giá trị: `DAILY`, `WEEKLY`, `MONTHLY`. Chỉ dùng với `INCREMENT`. **Loại trừ với `streak`** |
+| `options` | | Tùy chọn bổ sung. Với `APPEND`: `{ "unique": true, "maxSize": 50 }` |
+| `threshold` | | Điều kiện kích hoạt Trigger Event. Nếu không có thì Attribute chỉ lưu data, không trigger |
+
+## 10. Registry (Cơ chế Đăng ký Sự kiện)
+
+Toàn bộ "trí tuệ" phân luồng nằm tại cấu hình (Registry) của Event Processor.
+
+**Luồng ánh xạ:** `Thin Event (Client gửi)` ──> `Registry tra cứu` ──> `Fat Payload (Chứa Instructions)`.
+
+*Ví dụ Registry:* Khi Client gửi sự kiện `Login`, Registry cấu hình đính kèm 3 Instructions:
+
+1. `SET_TRUE_ONCE` cho cờ `first_login` — Trigger `First_Login` nếu lần đầu.
+2. `INCREMENT` với `streak: "DAILY"` cho `login_streak` — Trigger `Login_Streak_7_Days` khi đạt 7.
+3. `SET` cho `last_login_date = eventTime` — Trigger `Account_Reactivated` nếu `TIME_GAP_GT 90 days`.
+
+## 11. Payload Transformation
+
+Sự biến đổi của dữ liệu từ Client cho tới lúc phát thưởng:
+
+- **Thin Event (Từ Client):** Rất mỏng, chỉ chứa `{ event: "Login", customerId: "CUST-001", eventTime: "2026-07-16T10:30:00+07:00" }`. Không chứa rule hay logic.
+- **Fat Payload (Từ Event Processor gửi Kafka):** Chứa Thin Event + Kẹp thêm mảng các `Instructions` (lấy từ Registry). Để chỉ đạo Attribute Service phải tính toán cái gì.
+- **Trigger Event (Do Attribute publish vào events-topic):** Khi đạt Threshold, sinh ra một sự kiện rõ ràng. VD: `{ event: "Login_Streak_7_Days", customerId: "CUST-001" }`. Transaction Service chỉ cần đọc tên event này từ Kafka để trả thưởng.
+
+## 12. Bảng Ánh xạ (Mapping Use Cases Hiện Tại)
+
+| Use Case | Original Event | Operation | Instruction chi tiết | Consumer | Output |
+| :--- | :--- | :--- | :--- | :--- | :--- |
+| **UC-01 (First Login)** | `Login` | `SET_TRUE_ONCE` | `key: first_login` | `Attribute` ➔ `Transaction` | +100đ, In-app msg, OTT |
+| **UC-02 (Daily Login)** | `Login` | `INCREMENT` | `key: login_streak, streak: DAILY` | `Attribute` ➔ `Transaction` | +100đ / +500đ |
+| **UC-03 (View Item)** | `ViewItem` | `INCREMENT` + `APPEND` | `key: weekly_view_count, window: WEEKLY` + `key: viewed_items, unique: true, maxSize: 50` | `Attribute` ➔ `Transaction` | +50đ / Lưu log recommend |
+| **UC-04 (Transfer)** | `Transfer_Balance`| `N/A` (Direct) | — | `Transaction` | Debit/Credit ACID |
+| **UC-05 (Inactive Login)**| `Login` | `SET` | `key: last_login_date, threshold: TIME_GAP_GT 90d` | `Attribute` ➔ `Transaction` | Trigger Account_Reactivated |
+| **UC-06 (Welcome Offer)**| `Welcome_Offer` | `N/A` (Direct) | — | `Transaction` | +100đ |
+| **UC-07 (Update Profile)**| `Profile_Completed`| `N/A` (Direct) | — | `Transaction` | +100đ |
+| **UC-08 (Survey)** | `Survey_Completed`| `SET_TRUE_ONCE` | `key: survey_completed_xyz` | `Attribute` ➔ `Transaction` | Tặng voucher & Đánh dấu True |
+| **UC-09 (Redeem)** | `Redeem_Item` | `N/A` (Direct) | — | `Transaction` | Trừ điểm + Reserve item |
+| **UC-10 (Merchant Scan)**| `N/A` (Sync API) | `N/A` (Sync API) | — | `Transaction` | Đánh dấu voucher USED |
+| **UC-11 (Retention PW)** | `Cronjob_Run` | `N/A` (Noti) | — | `Notification` | Push/Email cảnh báo PW |
+| **UC-12 (Most Redeem)** | `Redeem_Item` | `INCREMENT` | `key: total_redeem_count` (tích lũy, không reset) | `Attribute` | Tăng bộ đếm thuần túy |
+| **UC-13 (Dormant User)** | `Cronjob_Run` | `N/A` (Direct) | — | `Transaction` | Tặng ưu đãi + Push Noti |
diff --git a/specs/archive/HLD_Use_Cases_Mapping.md b/specs/archive/HLD_Use_Cases_Mapping.md
new file mode 100644
index 0000000..2e4013f
--- /dev/null
+++ b/specs/archive/HLD_Use_Cases_Mapping.md
@@ -0,0 +1,145 @@
+# Tài liệu Thiết kế Giải pháp theo Use Cases (HLD - Use Cases Mapping)
+## Hệ thống Event-Driven Loyalty & Rewards
+
+Dựa trên phân tích yêu cầu nghiệp vụ (Use Cases Analysis), tài liệu này mô tả chi tiết phương án kỹ thuật để đáp ứng từng Use Case, đảm bảo hệ thống mở rộng tốt, không bị nghẽn cổ chai và duy trì tính toàn vẹn dữ liệu cho các giao dịch tài chính.
+
+---
+
+### 1. Bảng Ánh xạ Nghiệp vụ - Kỹ thuật (UC-to-Solution Mapping Table)
+
+Bảng dưới đây giúp đội ngũ BA và Developer nắm bắt nhanh mối quan hệ 1-1 giữa yêu cầu nghiệp vụ và giải pháp kỹ thuật tương ứng.
+
+| Mã UC / Tên Use Case | Giải pháp Kỹ thuật tương ứng | Thành phần liên quan (Components) |
+| :--- | :--- | :--- |
+| **UC-01** - First Login
*(Đăng nhập lần đầu)* | Gán cờ `first_login_flag` bằng lệnh `SET_TRUE_ONCE`. Thành công sẽ kích hoạt event `First_Login_Reward` để hệ thống cộng điểm và đẩy thông báo realtime. | `attribute-service`, `transaction-service`, Kafka (`tracking-topic`, `events-topic`), PostgreSQL (Attr DB) |
+| **UC-02** - Daily Login Tracking
*(Điểm danh hàng ngày)* | Cập nhật bộ đếm `current_streak` và `last_login_date`. Bỏ qua các sự kiện trùng lặp trong ngày (Idempotency). Khi đủ 7/30 ngày, trigger event trả thưởng. | `attribute-service`, `transaction-service`, Kafka (`tracking-topic`, `events-topic`) |
+| **UC-03** - View Item
*(Xem chi tiết quà)* | Cập nhật `item_view_count`, lưu lịch sử `recently_viewed` vào Cache. Cộng `weekly_view_count`, đủ 10 views sẽ trigger event cộng điểm. | `attribute-service`, Redis (Cache), Kafka (`tracking-topic`), PostgreSQL (Attr DB) |
+| **UC-04** - Transfer Balance
*(Chuyển điểm)* | Xử lý giao dịch trừ tiền người gửi, cộng tiền người nhận trong cùng 1 ACID Transaction. Bắn push notification cho cả hai bên. | `transaction-service`, `notification-service`, Kafka (`events-topic`), PostgreSQL (Txn DB) |
+| **UC-05** - Inactive Account Login
*(Login sau thời gian ngủ đông)*| Kèm cấu hình `CHECK_AND_UPDATE_TIMER` vào event Login. Nếu GAP >= 90 ngày, trigger event `Account_Reactivated` để đổi trạng thái và gửi noti Welcome Back. | `attribute-service`, `transaction-service`, Kafka (`tracking-topic`, `events-topic`) |
+| **UC-06** - Welcome Offer
*(Mở thẻ mới)* | Hệ thống phát hành thẻ (External) bắn event. `transaction-service` kiểm tra khóa bảo vệ (Idempotency) để đảm bảo chỉ thưởng 1 lần. | Hệ thống phát hành thẻ, `transaction-service`, Kafka (`events-topic`) |
+| **UC-07** - Update Profile
*(Hoàn tất hồ sơ)* | `profile-service` tự đánh giá độ hoàn thiện và bắn event. Core engine kiểm tra mốc thời gian (trước 30/09) để quyết định cộng điểm. | `profile-service`, `transaction-service`, Kafka (`events-topic`) |
+| **UC-08** - Survey
*(Hoàn thành khảo sát)* | Bắn 1 event nhưng định tuyến song song 2 luồng: (1) Cấp voucher và (2) Cập nhật cờ `survey_completed = true` để không hiển thị lại popup. | `transaction-service`, `attribute-service`, Kafka (`events-topic`, `tracking-topic`) |
+| **UC-09** - Redeem Item
*(Đổi quà)* | Kiểm tra số dư, trừ điểm và khóa tồn kho (reserve item) trong 1 giao dịch cơ sở dữ liệu nguyên tử (ACID DB Transaction). | `transaction-service`, `notification-service`, PostgreSQL (Txn DB) |
+| **UC-10** - Merchant Scan
*(Quét mã voucher)* | API Đồng bộ (Synchronous). Đổi trạng thái voucher sang USED (ACID). Thành công sẽ đẩy noti realtime cho người dùng qua Kafka. | Merchant App, `transaction-service`, API Gateway, PostgreSQL (Txn DB) |
+| **UC-11** - Retention
*(Mật khẩu sắp hết hạn)* | Scheduler chạy hằng ngày quét DB lọc danh sách `password_expiry_date`. Bắn thẳng thông báo vào Kafka mà không cần đi qua Core engine. | `marketing-service`, `notification-service`, Kafka (`noti-topic`) |
+| **UC-12** - Most Redeem Item
*(Sản phẩm đổi nhiều nhất)* | Event Processor sao chép luồng sự kiện đổi quà sang `tracking-topic` để cập nhật counter `total_redeem_count` trên thuộc tính sản phẩm (Thuần Tracking). | Event Processor, `attribute-service`, Kafka (`tracking-topic`), PostgreSQL (Attr DB) |
+| **UC-13** - Retention
*(User Dormant 3 tháng)* | Scheduler chạy hằng ngày lọc user có `last_user_txn_date` >= 90 ngày. Bắn event vào Core engine để thực thi cấp ưu đãi riêng và push noti. | `marketing-service`, `transaction-service`, PostgreSQL (Txn DB & Marketing DB) |
+
+---
+
+### 2. Cấu trúc chi tiết giải pháp theo từng UC (High-Level Technical Details)
+
+#### Nhóm 1: Hệ thống đếm và Theo dõi (Tracking & Counters)
+*Bao gồm: UC-01, UC-02, UC-03, UC-05, UC-12*
+- **Luồng dữ liệu (Data Flow):** Client -> `event-processor-service` -> `tracking-topic` -> `attribute-service`.
+- **Cơ chế xử lý đặc biệt:**
+ - **Pattern "Accumulate & Trigger":** `attribute-service` hoạt động như một "Dumb Executor" tích lũy các event có tần suất cao (như Login, ViewItem). Nó xử lý các quy tắc lũy kế, khi đạt ngưỡng (Threshold) hoặc điều kiện trạng thái, nó mới sản sinh một event "có giá trị tài chính" (ví dụ: `View_10_Items_Reward`) đẩy vào `events-topic` để `transaction-service` trả thưởng. Cách này bảo vệ Core Engine không bị quá tải.
+ - **Caching:** Dùng Redis để lưu trữ thuộc tính `recently_viewed` (UC-03) nhằm đáp ứng nhu cầu truy vấn hiển thị liên tục ở trang chủ với độ trễ cực thấp (<10ms).
+ - **Idempotency (Tính luỹ đẳng):** Bộ đếm tự động lọc bỏ các hành động lặp lại không hợp lệ, ví dụ login nhiều lần trong 1 ngày (UC-02) hoặc kiểm tra cờ (UC-01) để tránh cộng thưởng nhầm.
+
+#### Nhóm 2: Giao dịch Tài chính & Quản lý Kho (Financial & Inventory)
+*Bao gồm: UC-04, UC-09, UC-10*
+- **Luồng dữ liệu (Data Flow):**
+ - (UC-04, UC-09): Client -> `event-processor-service` -> `events-topic` -> `transaction-service`.
+ - (UC-10): Merchant App -> HTTP Sync API -> `transaction-service`.
+- **Cơ chế xử lý đặc biệt:**
+ - **ACID Transactions / Rollback:** Việc trừ điểm người gửi & cộng điểm người nhận (UC-04) hoặc trừ điểm & khóa tồn kho (UC-09) bắt buộc nằm chung trong 1 Database Transaction nguyên tử. Nếu 1 trong 2 thao tác lỗi, toàn bộ giao dịch bị rollback, đảm bảo không bao giờ có chuyện "trừ điểm nhưng chưa có quà".
+ - **Cơ chế gọi Đồng bộ (Sync API):** Ở UC-10, thay vì gửi qua Kafka, giao dịch quét voucher được thực thi trực tiếp qua Restful API để trả về kết quả thành công/thất bại ngay lập tức trên máy Pos của Merchant.
+
+#### Nhóm 3: Tương tác Hệ thống Ngoài & Luồng Song song
+*Bao gồm: UC-06, UC-07, UC-08*
+- **Luồng dữ liệu (Data Flow):** External Systems / App -> `event-processor-service` -> `events-topic` -> `transaction-service`.
+- **Cơ chế xử lý đặc biệt:**
+ - **Dual Publishing (Định tuyến song song):** Trong UC-08 (Hoàn thành khảo sát), Event Processor sao chép sự kiện thành 2 luồng độc lập: một vào `events-topic` để lấy voucher, một vào `tracking-topic` để ẩn màn hình khảo sát. Việc này giảm liên kết cứng (decouple) giữa UI State và Financial Ledger.
+
+#### Nhóm 4: Cronjob, Batch Processing & Retention
+*Bao gồm: UC-11, UC-13*
+- **Luồng dữ liệu (Data Flow):** `marketing-service` (Cronjob) -> Query Database -> Kafka -> Các Service liên quan.
+- **Cơ chế xử lý đặc biệt:**
+ - **Off-peak Processing:** Chạy các job quét CSDL vào giờ thấp điểm (đêm khuya) để lấy danh sách người dùng ngủ đông hoặc mật khẩu sắp hết hạn. Tránh chiếm dụng connection của DB chính.
+ - **Phân luồng:** UC-11 chỉ gửi tin nhắn nên đẩy thẳng vào `noti-topic`. UC-13 liên quan đến tặng quà nên định tuyến về `events-topic` để thực thi qua Core.
+
+---
+
+### 3. Sơ đồ Luồng Xử lý Kỹ thuật Tổng quát (System Flow Diagram)
+
+Sơ đồ dưới đây trình bày cấu trúc định tuyến thông minh (Smart Router) và cách các thành phần lõi trao đổi với nhau nhằm đáp ứng chuỗi sự kiện.
+
+```mermaid
+graph TD
+ %% Clients
+ MA[Member App / Web]
+ MRA[Merchant App]
+ EXT[External Systems: Card Issuance / Profile]
+
+ %% Gateway
+ subgraph Smart Event Gateway
+ EP{event-processor-service
Validate & Router}
+ end
+
+ %% Kafka Topics
+ subgraph Message Broker
+ K_ET([events-topic
Direct Financial])
+ K_TT([tracking-topic
Tracking & State])
+ K_NT([noti-topic
Notifications])
+ end
+
+ %% Core Services
+ subgraph Core Services
+ ATTR[attribute-service
EAV & Accumulator]
+ TP[transaction-service
Ledger & Rule Engine]
+ NS[notification-service
Push/SMS/Email]
+ MKT[marketing-service
Cron & Scheduler]
+ end
+
+ %% Databases
+ DB_ATTR[(PostgreSQL: Attribute)]
+ REDIS[(Redis: Cache)]
+ DB_TP[(PostgreSQL: Transaction)]
+
+ %% Connections
+ MA -- "Login, ViewItem, Redeem, Transfer" --> EP
+ EXT -- "WelcomeOffer, ProfileCompleted" --> EP
+ MRA -- "Sync API: Scan Voucher" --> TP
+
+ EP -- "Tracking/State Events" --> K_TT
+ EP -- "Financial/Reward Events" --> K_ET
+
+ K_TT --> ATTR
+ K_ET --> TP
+
+ ATTR -- "Read/Write" --> DB_ATTR
+ ATTR -- "Fast Read" --> REDIS
+ TP -- "ACID Txn" --> DB_TP
+
+ ATTR -. "Trigger Reward
(Threshold Reached)" .-> K_ET
+ TP -- "Result Noti" --> K_NT
+ MKT -- "Retention Noti" --> K_NT
+ MKT -- "Dormant Reward" --> K_ET
+
+ K_NT --> NS
+ NS -- "Firebase Push" --> MA
+```
+
+---
+
+### 4. Đánh giá Mức độ Khả thi & Rủi ro (Technical Risk Assessment)
+
+Trong 13 Use Cases, có những thách thức kỹ thuật cần đặc biệt lưu tâm và áp dụng chiến lược phòng vệ:
+
+**1. Rủi ro phức tạp nhất: Đổi quà (UC-09 - Redeem Item)**
+- **Vấn đề:** Rất dễ xảy ra lỗi tranh chấp dữ liệu (Race Condition) khi nhiều người dùng cùng đổi 1 món quà đang "hot" có số lượng giới hạn, hoặc timeout giao dịch DB khiến hệ thống "trừ điểm nhưng kho không trừ".
+- **Phương án dự phòng (Fallback & Mitigation):**
+ - Áp dụng cơ chế **Pessimistic Locking** (`SELECT FOR UPDATE`) ở bảng Inventory để đảm bảo nhất quán dữ liệu.
+ - Kết hợp sử dụng **Redis Decrement** để cấp phát số lượng kho tạm thời trên memory trước khi thực hiện giao dịch xuống Database (Pre-deduction), giảm tải I/O cho PostgreSQL.
+
+**2. Nghẽn cổ chai luồng sự kiện (Bottleneck in High-Frequency Events)**
+- **Vấn đề:** Các sự kiện như `View_Item` hay `Login` có lượng truy cập khổng lồ, nếu đẩy dồn dập có thể làm chậm trễ các sự kiện quan trọng như `Transfer_Balance` trên hạ tầng Kafka.
+- **Phương án dự phòng (Fallback & Mitigation):**
+ - Tách bạch hoàn toàn `tracking-topic` (Volume lớn, độ ưu tiên thấp) và `events-topic` (Volume vừa, độ ưu tiên cao).
+ - Tăng số lượng Phân vùng (Partitions) trên `tracking-topic` và chạy nhiều Instance (Consumer Group) của `attribute-service` để tiêu thụ dữ liệu song song (Horizontal Scale).
+
+**3. Cơ chế phục hồi sự cố (Fault Tolerance & Dead Letter Queue)**
+- **Vấn đề:** Khi `transaction-service` dính ngoại lệ logic (VD: Khách hàng bị khóa tài khoản ngay lúc event trả điểm tới) khiến việc consume báo lỗi liên tục.
+- **Phương án dự phòng (Fallback & Mitigation):**
+ - Cấu hình **Dead Letter Queue (DLQ)** trên Kafka. Nếu một sự kiện xử lý thất bại sau số lần retry quy định (VD: 3 lần), hệ thống sẽ gắp nó bỏ qua DLQ để tránh làm nghẽn dòng thông tin (Head-of-line blocking). Đội vận hành sẽ tái xử lý (replay) các sự kiện DLQ này sau khi khắc phục xong lỗi dữ liệu hoặc rule kinh doanh.
diff --git a/specs/archive/High_Level_Design.md b/specs/archive/High_Level_Design.md
new file mode 100644
index 0000000..f17eeb9
--- /dev/null
+++ b/specs/archive/High_Level_Design.md
@@ -0,0 +1,143 @@
+# Tài liệu Thiết kế Tổng quan (High-Level Design - HLD)
+## Hệ thống Event-Driven Loyalty & Rewards
+
+### 1. Tổng quan Hệ thống (System Overview)
+**Mục tiêu:** Xây dựng một nền tảng Loyalty linh hoạt, hiệu năng cao, dựa trên kiến trúc hướng sự kiện (Event-Driven). Hệ thống có khả năng xử lý song song các giao dịch tài chính (cộng/trừ điểm) an toàn tuyệt đối và theo dõi hành vi người dùng (tracking) ở tần suất cao mà không gây nghẽn luồng xử lý giao dịch cốt lõi.
+
+**Đối tượng và Hệ thống liên quan:**
+- **Actors (Người dùng):** Khách hàng (Member) sử dụng ứng dụng di động/web; Nhân viên đối tác (Merchant) sử dụng ứng dụng quét mã.
+- **External Systems (Hệ thống ngoài):** Hệ thống phát hành thẻ (Card Issuance), Hệ thống gửi tin (SMS/Email/Firebase).
+
+### 2. Sơ đồ Kiến trúc Tổng quan (Architecture Topology)
+Hệ thống được chia thành các phân lớp (layers) rõ ràng nhằm đảm bảo tính độc lập, dễ bảo trì và dễ dàng mở rộng.
+
+- **Client Layer:** Các ứng dụng tương tác trực tiếp với người dùng cuối (Member App, Merchant App).
+- **Event Gateway Layer:** `event-processor-service` đóng vai trò là "Smart Router", chịu trách nhiệm tiếp nhận, validate và phân loại sự kiện (Direct, Tracking, Notification) để định tuyến vào đúng Kafka Topic.
+- **Core Service Layer:**
+ - `transaction-service`: Core Engine đảm bảo tính ACID, xử lý giao dịch điểm/voucher và thực thi Rule logic.
+ - `attribute-service`: Quản lý thuộc tính động (EAV) và các bộ đếm (Counter), tích lũy sự kiện để tránh dội tải cho Core Engine.
+ - `notification-service`: Chuyên biệt cho việc gửi thông báo đa kênh.
+ - `marketing-service`: Chạy cronjob, scheduler cho các chiến dịch và tập khách hàng.
+ - `customer-service`: Quản lý hồ sơ người dùng.
+- **Message Broker Layer:** Sử dụng Apache Kafka làm xương sống giao tiếp bất đồng bộ, chia thành các topic chuyên biệt (`events-topic`, `tracking-topic`, `noti-topic`).
+- **Data & Caching Layer:** CSDL quan hệ (PostgreSQL) độc lập cho từng service, Cache (Redis) hỗ trợ truy xuất nhanh.
+
+```mermaid
+graph TD
+ subgraph Client Layer
+ MA[Member App]
+ MRA[Merchant App]
+ end
+
+ subgraph Event Gateway Layer
+ EP[event-processor-service
Smart Router]
+ end
+
+ subgraph Message Broker Layer
+ K_ET[Kafka: events-topic]
+ K_TT[Kafka: tracking-topic]
+ K_NT[Kafka: noti-topic]
+ end
+
+ subgraph Core Service Layer
+ TP[transaction-service
Core Rule & Ledger]
+ ATTR[attribute-service
Tracking & Counter]
+ NS[notification-service
Push/SMS]
+ MKT[marketing-service
Scheduler]
+ end
+
+ subgraph Data & Caching Layer
+ DB_TP[(PostgreSQL
Transaction)]
+ DB_ATTR[(PostgreSQL
EAV/Attribute)]
+ REDIS[(Redis
Cache)]
+ end
+
+ MA -->|Events: Login, View, Redeem| EP
+ MRA -.->|Sync API: Scan Voucher| TP
+
+ EP -->|Direct Events| K_ET
+ EP -->|Tracking Events| K_TT
+ EP -->|Noti Events| K_NT
+
+ K_ET --> TP
+ K_TT --> ATTR
+ K_NT --> NS
+
+ MKT -->|Cron: Scheduled Events| EP
+ ATTR -->|Threshold Reached Trigger| K_ET
+ TP -->|Reward Result| K_NT
+
+ TP --- DB_TP
+ ATTR --- DB_ATTR
+ ATTR -.-> REDIS
+```
+
+### 3. Luồng dữ liệu & Tương tác chính (Key Workflows & Sequence)
+
+#### 3.1. Luồng "Accumulate & Trigger" - Tracking & Thưởng (Ví dụ: View Item)
+**Mô tả:** Nhằm tránh làm nghẽn Core Engine bởi các sự kiện có tần suất cao, hệ thống sử dụng Attribute Service để đếm (Counter). Chỉ khi đạt ngưỡng quy định (Threshold), một sự kiện trả thưởng mới được sinh ra và đẩy cho Core Engine xử lý.
+
+```mermaid
+sequenceDiagram
+ participant MA as Member App
+ participant EP as Event Processor
+ participant K_TT as Kafka (tracking-topic)
+ participant ATTR as Attribute Service
+ participant K_ET as Kafka (events-topic)
+ participant TP as Transaction Service
+
+ MA->>EP: Gửi Event (VD: ViewItem)
+ EP->>EP: Phân loại: Tracking Event
+ EP->>K_TT: Đẩy vào tracking-topic
+ K_TT->>ATTR: Consume Event
+
+ ATTR->>ATTR: 1. Cập nhật Counter (weekly_view_count++)
2. Cập nhật Cache (recently_viewed)
+
+ alt Counter >= Threshold (VD: 10)
+ ATTR->>K_ET: Bắn Event Trả Thưởng (View_10_Items_Reward)
+ ATTR->>ATTR: Reset Counter
+ K_ET->>TP: Consume Reward Event
+ TP->>TP: Cộng điểm (Transaction ACID)
+ end
+```
+
+#### 3.2. Luồng Xử lý Giao dịch Tài chính Trực tiếp (Ví dụ: Redeem Item / Transfer)
+**Mô tả:** Các giao dịch mang tính tài chính trực tiếp (chuyển điểm, đổi quà) được gọi trực tiếp qua API tới Transaction Service để xử lý đồng bộ (Synchronous), giúp đảm bảo tốc độ phản hồi ngay lập tức cho người dùng và tính toàn vẹn dữ liệu.
+
+```mermaid
+sequenceDiagram
+ participant MA as Member App
+ participant TP as Transaction Service
+ participant K_NT as Kafka (noti-topic)
+ participant NS as Notification Service
+
+ MA->>TP: Gọi API trực tiếp (Redeem_Item / Transfer)
+
+ TP->>TP: BEGIN DB TRANSACTION
+ TP->>TP: Validate Số dư & Điều kiện
+ TP->>TP: Debit (-) Điểm & Cập nhật State/Inventory
+ TP->>TP: COMMIT TRANSACTION
+
+ TP-->>MA: Trả kết quả thành công (200 OK)
+
+ TP->>K_NT: Bắn Event Giao dịch Thành công
+ K_NT->>NS: Consume Event
+ NS->>MA: Đẩy Noti (Firebase Push/SMS)
+```
+
+### 4. Lựa chọn Công nghệ & Hạ tầng (Technology Stack & Infrastructure)
+- **Backend (Microservices):** Sử dụng Java / Spring Boot. Lý do: Hệ sinh thái phong phú, độ ổn định cực cao (Enterprise-grade) chuyên trị các hệ thống tài chính/giao dịch khắt khe.
+- **Message Broker:** Apache Kafka. Lý do: Khả năng xử lý thông lượng khổng lồ (high-throughput), độ trễ cực thấp, hỗ trợ cơ chế lưu trữ bền vững (persistence) và phân chia luồng qua Consumer Groups.
+- **Database Chính:** PostgreSQL. Lý do: Khả năng hỗ trợ ACID mạnh mẽ cho Transaction Service, đồng thời hỗ trợ kiểu dữ liệu JSONB tối ưu cho mô hình EAV (Entity-Attribute-Value) của Attribute Service.
+- **Caching:** Redis. Lý do: Tốc độ Read/Write in-memory cực nhanh, phù hợp cho việc lưu trữ cache dữ liệu có tần suất đọc cao phi tài chính (ví dụ: recently viewed items).
+- **Data Warehouse:** Doris. Lý do: Phục vụ bóc tách dữ liệu báo cáo, phân tích (BI) và phân tập khách hàng (Segmentation) mà không làm suy giảm hiệu năng của hệ thống CSDL tác nghiệp (OLTP).
+
+### 5. Giải pháp Đảm bảo Chất lượng (Non-Functional Highlights)
+- **Hiệu năng & Mở rộng (Performance & Scalability):**
+ - **Tách bạch luồng dữ liệu (Isolation):** Việc tách riêng luồng giao dịch cốt lõi (Direct) và luồng theo dõi hành vi (Tracking) giúp Core Engine tập trung 100% công suất cho dòng tiền/điểm.
+ - **Event-Driven & Async processing:** Các luồng không yêu cầu phản hồi ngay được đẩy qua Kafka, cho phép các service tự điều chỉnh tốc độ tiêu thụ (throttle) và dễ dàng mở rộng ngang (horizontal scaling) thông qua việc tăng partition của Kafka.
+- **Bảo mật & Tính toàn vẹn (Security & Data Integrity):**
+ - **Idempotency (Tính luỹ đẳng):** Transaction Service đảm bảo mọi giao dịch cộng/trừ điểm hoặc cấp thẻ/voucher đều kiểm tra khóa Idempotent, chống duplicate event sinh ra từ mạng chập chờn.
+ - **ACID Transaction:** Mọi biến động điểm số hay trạng thái kho (voucher) đều được thực thi và khóa chặt trong 1 Database Transaction nguyên tử.
+- **Tính khả dụng & Giám sát (Availability & Monitoring):**
+ - **Smart Routing & Fallback:** Event Processor kiểm tra định dạng Schema của từng sự kiện đầu vào. Các sự kiện lỗi định dạng sẽ bị reject hoặc đẩy vào Dead Letter Queue (DLQ) để điều tra và phục hồi sau, đảm bảo không làm sập các service thụ hưởng phía sau.
diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts
index eeea9f6..b8a64a1 100644
--- a/src/mocks/handlers.ts
+++ b/src/mocks/handlers.ts
@@ -5,7 +5,7 @@ let events: Event[] = [
{
id: 'event-login-unified',
name: 'Login',
- type: 'COUNTER',
+ type: 'TRACKING',
targetTopic: 'tracking-topic',
attributes: [
{ id: 'attr-login-1', key: 'customerId', displayName: 'Mã Khách Hàng', type: 'STRING', required: true }
@@ -45,7 +45,7 @@ let events: Event[] = [
{
id: 'uc-03',
name: 'View_Item',
- type: 'COUNTER',
+ type: 'TRACKING',
targetTopic: 'tracking-topic',
attributes: [
{ id: 'attr-uc03-1', key: 'customerId', displayName: 'Mã Khách Hàng', type: 'STRING', required: true },
@@ -122,7 +122,7 @@ let events: Event[] = [
{
id: 'uc-08',
name: 'Survey_Completed',
- type: 'COUNTER',
+ type: 'TRACKING',
targetTopic: 'events-topic',
transactionCode: 'SURVEY_REWARD',
attributes: [
@@ -178,7 +178,7 @@ let events: Event[] = [
{
id: 'uc-12',
name: 'Item_Redeemed_Count',
- type: 'COUNTER',
+ type: 'TRACKING',
targetTopic: 'tracking-topic',
attributes: [
{ id: 'attr-uc12-1', key: 'itemId', type: 'STRING', required: true }
diff --git a/src/pages/EventForm.tsx b/src/pages/EventForm.tsx
index f289b8a..7b5c210 100644
--- a/src/pages/EventForm.tsx
+++ b/src/pages/EventForm.tsx
@@ -26,7 +26,7 @@ import { Separator } from '@/components/ui/separator';
const eventSchema = z.object({
name: z.string().min(1, 'Name is required'),
- type: z.enum(['DIRECT', 'COUNTER', 'NOTIFICATION']),
+ type: z.enum(['DIRECT', 'TRACKING', 'NOTIFICATION']),
targetTopic: z.string(),
transactionCode: z.string().optional(),
notificationId: z.string().optional(),
@@ -81,7 +81,7 @@ export function EventForm() {
useEffect(() => {
if (eventType === 'DIRECT') setValue('targetTopic', 'events-topic');
- else if (eventType === 'COUNTER') setValue('targetTopic', 'tracking-topic');
+ else if (eventType === 'TRACKING') setValue('targetTopic', 'tracking-topic');
else if (eventType === 'NOTIFICATION') setValue('targetTopic', 'noti-topic');
}, [eventType, setValue]);
@@ -158,7 +158,7 @@ export function EventForm() {
value={field.value}
items={{
DIRECT: 'Direct',
- COUNTER: 'Counter',
+ TRACKING: 'Tracking',
NOTIFICATION: 'Notification'
}}
>
@@ -167,7 +167,7 @@ export function EventForm() {
Direct
- Counter
+ Tracking
Notification
@@ -231,11 +231,11 @@ export function EventForm() {
- {eventType === 'COUNTER' && (
+ {eventType === 'TRACKING' && (
- Counter Instructions
- Configure rules for incrementing counters and triggering subsequent events.
+ Tracking Instructions
+ Configure rules for tracking attributes and triggering subsequent events.
diff --git a/src/pages/EventsList.tsx b/src/pages/EventsList.tsx
index 383143a..172845d 100644
--- a/src/pages/EventsList.tsx
+++ b/src/pages/EventsList.tsx
@@ -147,7 +147,7 @@ export function EventsList() {
items={{
ALL: 'All Types',
DIRECT: 'Direct',
- COUNTER: 'Counter',
+ TRACKING: 'Tracking',
NOTIFICATION: 'Notification'
}}
>
@@ -157,7 +157,7 @@ export function EventsList() {
All Types
Direct
- Counter
+ Tracking
Notification
@@ -189,10 +189,10 @@ export function EventsList() {
{event.name}
diff --git a/src/types/event.ts b/src/types/event.ts
index 0b7976f..29e7a85 100644
--- a/src/types/event.ts
+++ b/src/types/event.ts
@@ -1,4 +1,4 @@
-export type EventType = 'DIRECT' | 'COUNTER' | 'NOTIFICATION';
+export type EventType = 'DIRECT' | 'TRACKING' | 'NOTIFICATION';
export type AttributeType = 'STRING' | 'NUMBER' | 'BOOLEAN' | 'DATETIME';
export type OperationType = 'INCREMENT' | 'STREAK_INCREMENT' | 'COMPUTE_GAP' | 'APPEND_UNIQUE' | 'SET_TRUE_ONCE' | 'CHECK_AND_UPDATE_TIMER';