# 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 |