feat: rename COUNTER to TRACKING and update event type documentation and HLD architecture specs

This commit is contained in:
2026-07-29 14:26:39 +07:00
parent f33a1e05ad
commit 064ef0cb3c
10 changed files with 2136 additions and 223 deletions

View File

@@ -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<br/>Validate + Registry + Route]
API[TP Sync API]
end
subgraph Kafka
ET[events-topic<br/>Direct Events / Reward Triggers]
TT[tracking-topic<br/>Tracking Events]
NT[noti-topic<br/>Notification Events]
DLQ[dead-letter-topics]
end
subgraph Core Services
TP[transaction-service<br/>Rule + Ledger + Inventory]
ATTR[attribute-service<br/>State + Counters]
NS[notification-service]
MKT[marketing-service]
end
subgraph Data Layer
DB_TP[(PostgreSQL<br/>Transaction DB)]
DB_ATTR[(PostgreSQL<br/>Attribute DB)]
REDIS[(Redis<br/>Cache)]
DORIS[(Doris<br/>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<br/>via Outbox| ET
TP -->|Reward / Txn Result<br/>via Outbox| NT
TP -->|Post-transaction Tracking Event<br/>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``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``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``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``ONCE_PER_PERIOD` cho giai đoạn hiện tại.