diff --git a/services/membership-service-net/docs/en/ARCHITECTURE.md b/services/membership-service-net/docs/en/ARCHITECTURE.md
index 5541a661..97967076 100644
--- a/services/membership-service-net/docs/en/ARCHITECTURE.md
+++ b/services/membership-service-net/docs/en/ARCHITECTURE.md
@@ -2,7 +2,7 @@
## System Overview
-Membership Service manages user membership data. Profile information is stored in IAM Service.
+Membership Service manages user membership data including **Level**, **Experience (EXP)**, and **Preferences**. Profile information is stored in IAM Service.
```mermaid
graph TB
@@ -17,8 +17,9 @@ graph TB
subgraph Services["Microservices"]
IAM["🔐 IAM Service
(User & Profile)"]
- MEMBER["👥 Membership Service
(Membership Data)"]
+ MEMBER["👥 Membership Service
(Level, EXP, Preferences)"]
STORAGE["📦 Storage Service"]
+ WALLET["💰 Wallet Service"]
end
subgraph Data["Data Layer"]
@@ -34,17 +35,20 @@ graph TB
IAM --> |"JWT Token"| MEMBER
MEMBER --> |"HTTP Client"| IAM
MEMBER --> PG
+ MEMBER -.-> |"Level Up Event"| WALLET
style IAM fill:#3b82f6,stroke:#1e40af,color:#fff
style MEMBER fill:#22c55e,stroke:#15803d,color:#fff
style STORAGE fill:#eab308,stroke:#a16207,color:#000
+ style WALLET fill:#a855f7,stroke:#7e22ce,color:#fff
style TRAEFIK fill:#6366f1,stroke:#4338ca,color:#fff
```
> [!IMPORTANT]
> **Responsibility Separation:**
> - **IAM Service**: Manages User identity and UserProfile (avatar, phone, address, DOB)
-> - **Membership Service**: Manages membership data (membership level, preferences, country code, gender)
+> - **Membership Service**: Manages membership data (level, experience, preferences, country code, gender)
+> - **Wallet Service**: Receives Level Up events to process rewards
## Clean Architecture Layers
@@ -55,6 +59,7 @@ graph TB
├────────────────────────────────────────────────────────────┤
│ Domain Layer (Core) │
│ Entities, Aggregates, Value Objects, Domain Events │
+│ Member Aggregate, LevelDefinition Aggregate │
├────────────────────────────────────────────────────────────┤
│ Infrastructure Layer │
│ DbContext, Repositories, Entity Configurations │
@@ -64,7 +69,7 @@ graph TB
## Domain Model
-### Aggregate Structure
+### Aggregates Overview
```mermaid
classDiagram
@@ -73,37 +78,115 @@ classDiagram
+Guid UserId
+string CountryCode
+string? Gender
- +int MembershipLevelId
- +MembershipLevel MembershipLevel
+ +int CurrentLevel
+ +int CurrentExp
+ +int TotalExpEarned
+string? Preferences
+bool IsDeleted
+DateTime CreatedAt
+DateTime UpdatedAt
+ +AddExperience(points, levelRules)
+UpdateGender()
+UpdateCountryCode()
+UpdatePreferences()
- +ChangeMembershipLevel()
+Delete()
+Restore()
}
- class MembershipLevel {
- <>
- +int Id
+ class LevelDefinition {
+ +Guid Id
+ +int LevelNumber
+string Name
- +Free = 1
- +Basic = 2
- +Premium = 3
+ +int RequiredExp
+ +string? Description
+ +string? IconUrl
+ +string? Benefits
+ +bool IsActive
+ +DateTime CreatedAt
+ +DateTime UpdatedAt
}
- Member --> MembershipLevel
+ Member ..> LevelDefinition : uses rules
style Member fill:#22c55e,stroke:#15803d,color:#fff
- style MembershipLevel fill:#3b82f6,stroke:#1e40af,color:#fff
+ style LevelDefinition fill:#3b82f6,stroke:#1e40af,color:#fff
+```
+
+### Member Aggregate (Rich Domain Model)
+
+```csharp
+public class Member : Entity, IAggregateRoot
+{
+ // Private fields for encapsulation
+ private int _currentLevel;
+ private int _currentExp;
+ private int _totalExpEarned;
+
+ public int CurrentLevel => _currentLevel;
+ public int CurrentExp => _currentExp;
+ public int TotalExpEarned => _totalExpEarned;
+
+ ///
+ /// EN: Add experience and automatically level up if thresholds are met.
+ /// VI: Thêm exp và tự động lên level nếu đạt ngưỡng.
+ ///
+ public void AddExperience(int points, IReadOnlyList levelRules)
+ {
+ if (points <= 0)
+ throw new DomainException("EXP points must be positive");
+
+ int oldLevel = _currentLevel;
+ _currentExp += points;
+ _totalExpEarned += points;
+ _updatedAt = DateTime.UtcNow;
+
+ // Business logic: Calculate new level
+ var newLevel = CalculateLevel(_currentExp, levelRules);
+
+ if (newLevel > oldLevel)
+ {
+ _currentLevel = newLevel;
+ // Raise domain event for side effects
+ AddDomainEvent(new MemberLevelUpDomainEvent(this, oldLevel, newLevel));
+ }
+
+ AddDomainEvent(new MemberExperienceAddedDomainEvent(this, points));
+ }
+
+ private static int CalculateLevel(int exp, IReadOnlyList rules)
+ {
+ return rules
+ .Where(r => r.IsActive && r.RequiredExp <= exp)
+ .OrderByDescending(r => r.LevelNumber)
+ .FirstOrDefault()?.LevelNumber ?? 1;
+ }
+}
```
> [!NOTE]
-> Member entity does not store profile fields (phone, avatar, address, DOB). These fields are managed by IAM Service's UserProfile.
+> **Rich Domain Model**: Business logic for EXP and Level is encapsulated inside the Member entity, not in the Service Layer. This avoids the "Anemic Domain Model" anti-pattern.
+
+### LevelDefinition Aggregate (Customizable Levels)
+
+```csharp
+public class LevelDefinition : Entity, IAggregateRoot
+{
+ public int LevelNumber { get; private set; } // 1, 2, 3...
+ public string Name { get; private set; } // "Bronze", "Silver", "Gold"
+ public int RequiredExp { get; private set; } // 0, 100, 300, 600...
+ public string? Description { get; private set; }
+ public string? IconUrl { get; private set; }
+ public string? Benefits { get; private set; } // JSON benefits
+ public bool IsActive { get; private set; }
+
+ public void UpdateRequiredExp(int newRequiredExp)
+ {
+ if (newRequiredExp < 0)
+ throw new DomainException("Required EXP cannot be negative");
+ RequiredExp = newRequiredExp;
+ }
+}
+```
### Domain Events
@@ -111,53 +194,102 @@ classDiagram
|-------|---------|---------|
| `MemberCreatedDomainEvent` | New member created | Welcome email, analytics |
| `MemberUpdatedDomainEvent` | Profile updated | Sync to other services |
-| `MembershipLevelChangedDomainEvent` | Level changed | Billing, benefits update |
+| `MemberExperienceAddedDomainEvent` | EXP received | Logging, analytics |
+| `MemberLevelUpDomainEvent` | Level up | **Rewards, notifications, unlock features** |
+
+```mermaid
+sequenceDiagram
+ participant Client
+ participant Handler
+ participant Member
+ participant EventDispatcher
+ participant WalletService
+
+ Client->>Handler: AddExperienceCommand
+ Handler->>Member: AddExperience(100, rules)
+ Member->>Member: _currentExp += 100
+ Member->>Member: CalculateLevel()
+
+ alt Level Up
+ Member->>Member: AddDomainEvent(LevelUpEvent)
+ end
+
+ Handler->>EventDispatcher: SaveEntitiesAsync()
+ EventDispatcher->>WalletService: Publish LevelUpEvent
+ WalletService-->>WalletService: Grant Rewards
+```
## CQRS Pattern
+### Write Side (Commands)
+
```mermaid
flowchart LR
- subgraph Commands["✏️ Write Side"]
- CMD1["CreateMemberCommand"]
- CMD2["UpdateMemberProfileCommand"]
- CMD3["ChangeMembershipLevelCommand"]
+ subgraph Commands["✏️ Commands"]
+ CMD1["AddExperienceCommand"]
+ CMD2["CreateMemberCommand"]
+ CMD3["UpdateMemberProfileCommand"]
+ CMD4["CreateLevelDefinitionCommand"]
end
subgraph Handlers["⚙️ Command Handlers"]
- H1["CreateMemberHandler"]
- H2["UpdateProfileHandler"]
- H3["ChangeLevelHandler"]
+ H1["AddExperienceHandler"]
+ H2["CreateMemberHandler"]
+ H3["UpdateProfileHandler"]
+ H4["CreateLevelHandler"]
end
- subgraph Queries["📖 Read Side"]
- Q1["GetMemberByIdQuery"]
- Q2["GetMembersQuery"]
- end
-
- subgraph QueryHandlers["🔍 Query Handlers"]
- QH1["GetMemberByIdHandler"]
- QH2["GetMembersHandler"]
+ subgraph Domain["🏛️ Domain Model"]
+ DM["Rich Domain Model
EF Core"]
end
DB[("🐘 Database")]
- CMD1 --> H1 --> DB
- CMD2 --> H2 --> DB
- CMD3 --> H3 --> DB
-
- Q1 --> QH1 --> DB
- Q2 --> QH2 --> DB
+ CMD1 --> H1 --> DM --> DB
+ CMD2 --> H2 --> DM
+ CMD3 --> H3 --> DM
+ CMD4 --> H4 --> DM
style CMD1 fill:#ef4444,stroke:#b91c1c,color:#fff
style CMD2 fill:#ef4444,stroke:#b91c1c,color:#fff
style CMD3 fill:#ef4444,stroke:#b91c1c,color:#fff
+ style CMD4 fill:#ef4444,stroke:#b91c1c,color:#fff
+```
+
+### Read Side (Queries)
+
+```mermaid
+flowchart LR
+ subgraph Queries["📖 Queries"]
+ Q1["GetMemberByIdQuery"]
+ Q2["GetMemberProgressQuery"]
+ Q3["GetLevelDefinitionsQuery"]
+ end
+
+ subgraph Handlers["🔍 Query Handlers"]
+ QH1["GetMemberByIdHandler"]
+ QH2["GetMemberProgressHandler"]
+ QH3["GetLevelDefinitionsHandler"]
+ end
+
+ subgraph ReadModel["📊 Read Model"]
+ RM["Dapper / Raw SQL"]
+ end
+
+ DB[("🐘 Database")]
+
+ Q1 --> QH1 --> RM --> DB
+ Q2 --> QH2 --> RM
+ Q3 --> QH3 --> RM
+
style Q1 fill:#22c55e,stroke:#15803d,color:#fff
style Q2 fill:#22c55e,stroke:#15803d,color:#fff
+ style Q3 fill:#22c55e,stroke:#15803d,color:#fff
```
## Request Flow
-### Create Member Flow
+### Add Experience Flow
```mermaid
sequenceDiagram
@@ -165,23 +297,58 @@ sequenceDiagram
participant Controller
participant MediatR
participant Handler
- participant Repository
+ participant MemberRepo
+ participant LevelRepo
participant DB
- Client->>Controller: POST /api/v1/members
- Controller->>MediatR: Send(CreateMemberCommand)
+ Client->>Controller: POST /api/v1/members/{id}/experience
+ Note over Client,Controller: { "points": 100, "source": "purchase" }
+
+ Controller->>MediatR: Send(AddExperienceCommand)
MediatR->>Handler: Handle(command)
- Handler->>Repository: ExistsByUserIdAsync(userId)
- Repository->>DB: SELECT EXISTS
- DB-->>Repository: false
- Handler->>Repository: Add(member)
- Handler->>Repository: SaveEntitiesAsync()
- Repository->>DB: INSERT + Domain Events
- DB-->>Repository: OK
- Repository-->>Handler: member
- Handler-->>MediatR: CreateMemberResult
- MediatR-->>Controller: result
- Controller-->>Client: 201 Created
+
+ Handler->>MemberRepo: GetAsync(memberId)
+ MemberRepo->>DB: SELECT member
+ DB-->>MemberRepo: member data
+
+ Handler->>LevelRepo: GetAllActiveAsync()
+ LevelRepo->>DB: SELECT level_definitions
+ DB-->>LevelRepo: level rules
+
+ Handler->>Handler: member.AddExperience(100, rules)
+ Note over Handler: Business logic in Domain
+
+ Handler->>MemberRepo: UnitOfWork.SaveEntitiesAsync()
+ MemberRepo->>DB: UPDATE + Dispatch Domain Events
+
+ DB-->>Handler: OK
+ Handler-->>Controller: AddExperienceResult
+ Controller-->>Client: 200 OK
+ Note over Client,Controller: { "currentExp": 150, "currentLevel": 2, "leveledUp": true }
+```
+
+### Get Member Progress Flow
+
+```mermaid
+sequenceDiagram
+ participant Client
+ participant Controller
+ participant Handler
+ participant Dapper
+ participant DB
+
+ Client->>Controller: GET /api/v1/members/{id}/progress
+ Controller->>Handler: GetMemberProgressQuery
+ Handler->>Dapper: Raw SQL Query
+
+ Note over Dapper,DB: SELECT m.current_exp, m.current_level,
l.name, l.required_exp, next_level.required_exp
FROM members m
JOIN level_definitions l ON ...
+
+ Dapper->>DB: Execute
+ DB-->>Dapper: Result
+ Dapper-->>Handler: MemberProgressDto
+ Handler-->>Controller: Result
+ Controller-->>Client: 200 OK
+ Note over Client,Controller: { "currentLevel": 2, "levelName": "Silver",
"currentExp": 150, "expToNextLevel": 150,
"progressPercent": 50 }
```
## Database Schema
@@ -192,35 +359,107 @@ erDiagram
uuid id PK "Same as IAM UserId"
varchar country_code "ISO 3166-1 alpha-2"
varchar gender "male/female/other"
- int membership_level_id FK
+ int current_level "Current member level"
+ int current_exp "Current experience points"
+ int total_exp_earned "Total EXP ever earned"
jsonb preferences "User preferences"
boolean is_deleted "Soft delete flag"
timestamp created_at
timestamp updated_at
}
- MEMBERSHIP_LEVELS {
- int id PK
- varchar name
+ LEVEL_DEFINITIONS {
+ uuid id PK
+ int level_number UK "Level 1, 2, 3..."
+ varchar name "Bronze, Silver, Gold..."
+ int required_exp "EXP needed for this level"
+ text description
+ varchar icon_url
+ jsonb benefits "Level benefits/rewards"
+ boolean is_active
+ timestamp created_at
+ timestamp updated_at
}
- MEMBERS }o--|| MEMBERSHIP_LEVELS : has
+ MEMBERS }o..|| LEVEL_DEFINITIONS : "current_level references"
+```
+
+### SQL Schema
+
+```sql
+-- Level Definitions table (Admin configurable)
+CREATE TABLE level_definitions (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ level_number INT NOT NULL UNIQUE,
+ name VARCHAR(100) NOT NULL,
+ required_exp INT NOT NULL DEFAULT 0,
+ description TEXT,
+ icon_url VARCHAR(500),
+ benefits JSONB,
+ is_active BOOLEAN NOT NULL DEFAULT TRUE,
+ created_at TIMESTAMP NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMP NOT NULL DEFAULT NOW()
+);
+
+-- Members table
+CREATE TABLE members (
+ id UUID PRIMARY KEY,
+ country_code VARCHAR(2) NOT NULL DEFAULT 'VN',
+ gender VARCHAR(10),
+ current_level INT NOT NULL DEFAULT 1,
+ current_exp INT NOT NULL DEFAULT 0,
+ total_exp_earned INT NOT NULL DEFAULT 0,
+ preferences JSONB,
+ is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
+ created_at TIMESTAMP NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMP NOT NULL DEFAULT NOW()
+);
+
+-- Indexes
+CREATE INDEX ix_members_current_level ON members(current_level);
+CREATE INDEX ix_members_current_exp ON members(current_exp);
+CREATE INDEX ix_level_definitions_level_number ON level_definitions(level_number);
+CREATE INDEX ix_level_definitions_is_active ON level_definitions(is_active);
+
+-- Default Level Definitions
+INSERT INTO level_definitions (level_number, name, required_exp, description, is_active)
+VALUES
+ (1, 'Bronze', 0, 'Starting level', true),
+ (2, 'Silver', 100, 'Reach 100 EXP', true),
+ (3, 'Gold', 300, 'Reach 300 EXP', true),
+ (4, 'Platinum', 600, 'Reach 600 EXP', true),
+ (5, 'Diamond', 1000, 'Reach 1000 EXP', true);
```
## API Design
### RESTful Endpoints
+#### Member Endpoints
+
```
GET /api/v1/members # List (paginated, searchable)
GET /api/v1/members/{id} # Get by ID
GET /api/v1/members/me # Get current user
+GET /api/v1/members/{id}/progress # Get level progress
POST /api/v1/members # Create
+POST /api/v1/members/{id}/experience # Add EXP
PUT /api/v1/members/{id} # Update profile
-PUT /api/v1/members/{id}/level # Change level
```
-### Response Format
+#### Level Endpoints (Admin)
+
+```
+GET /api/v1/levels # List level definitions
+GET /api/v1/levels/{id} # Get level definition
+POST /api/v1/levels # Create level definition (Admin)
+PUT /api/v1/levels/{id} # Update level definition (Admin)
+DELETE /api/v1/levels/{id} # Deactivate level (Admin)
+```
+
+### Response Formats
+
+#### Member Response
```json
{
@@ -228,76 +467,113 @@ PUT /api/v1/members/{id}/level # Change level
"userId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"countryCode": "VN",
"gender": "male",
- "preferences": "{\"theme\": \"dark\", \"language\": \"vi\"}",
- "membershipLevel": {
- "id": 1,
- "name": "Free"
- },
+ "currentLevel": 2,
+ "currentExp": 150,
+ "totalExpEarned": 150,
+ "preferences": "{\"theme\": \"dark\"}",
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T10:30:00Z"
}
```
+#### Member Progress Response
+
+```json
+{
+ "memberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
+ "currentLevel": 2,
+ "levelName": "Silver",
+ "levelIconUrl": "/icons/silver.png",
+ "currentExp": 150,
+ "expToNextLevel": 150,
+ "nextLevelRequiredExp": 300,
+ "progressPercent": 50.0
+}
+```
+
+#### Add Experience Response
+
+```json
+{
+ "currentExp": 250,
+ "currentLevel": 2,
+ "totalExpEarned": 250,
+ "leveledUp": false,
+ "newLevel": null
+}
+```
+
+#### Level Definition Response
+
+```json
+{
+ "id": "a1b2c3d4-...",
+ "levelNumber": 3,
+ "name": "Gold",
+ "requiredExp": 300,
+ "description": "Reach 300 EXP to unlock Gold status",
+ "iconUrl": "/icons/gold.png",
+ "benefits": {
+ "discountPercent": 10,
+ "prioritySupport": true,
+ "freeShipping": false
+ },
+ "isActive": true
+}
+```
+
## Inter-Service Communication
### IAM Service Client
-Membership Service communicates with IAM Service through HTTP Client:
+Membership Service communicates with IAM Service through HTTP Client to validate users.
+
+### Integration Events (Planned)
+
+When a member levels up, publish Integration Event for other services to process:
```mermaid
-sequenceDiagram
- participant Membership
- participant IamClient["IIamServiceClient"]
- participant Cache["In-Memory Cache"]
- participant IAM["IAM Service"]
+flowchart LR
+ MEMBER["👥 Membership Service"]
+ BUS["📬 Event Bus
(RabbitMQ)"]
+ WALLET["💰 Wallet Service"]
+ NOTIF["🔔 Notification Service"]
- Membership->>IamClient: ValidateUserAsync(token)
- IamClient->>Cache: TryGetFromCache(cacheKey)
+ MEMBER -->|MemberLevelUpIntegrationEvent| BUS
+ BUS -->|Subscribe| WALLET
+ BUS -->|Subscribe| NOTIF
- alt Cache Hit
- Cache-->>IamClient: cached user info
- else Cache Miss
- IamClient->>IAM: GET /api/v1/users/me
- IAM-->>IamClient: user info
- IamClient->>Cache: AddToCache(user, 5min)
- end
-
- IamClient-->>Membership: IamUserInfo
+ WALLET -->|Grant Rewards| WALLET
+ NOTIF -->|Send Notification| NOTIF
+
+ style MEMBER fill:#22c55e,stroke:#15803d,color:#fff
+ style BUS fill:#ef4444,stroke:#b91c1c,color:#fff
+ style WALLET fill:#a855f7,stroke:#7e22ce,color:#fff
+ style NOTIF fill:#eab308,stroke:#a16207,color:#000
```
-### IAM Client Features
-
-| Feature | Description |
-|---------|-------------|
-| User Validation | Validate token and get user info |
-| Role/Permission Check | Check user roles and permissions |
-| Health Check | Check IAM Service availability |
-| Caching | In-memory cache with 5-minute TTL |
-
## Security
-### Authentication Flow
-
-```mermaid
-sequenceDiagram
- participant Client
- participant Traefik
- participant Membership
- participant IAM
-
- Client->>IAM: Login
- IAM-->>Client: JWT Token
- Client->>Traefik: Request + Bearer Token
- Traefik->>Membership: Forward request
- Membership->>Membership: Validate JWT
- Membership-->>Client: Response
-```
-
### Authorization
- All endpoints require JWT Bearer authentication
- Token validated against IAM Service authority
- UserId extracted from JWT claims (`sub` or `id`)
+- **Admin endpoints** (`/api/v1/levels` POST/PUT/DELETE) require `admin` role
+
+### Experience Source Validation
+
+```csharp
+// Validate experience source to prevent abuse
+public enum ExperienceSource
+{
+ Purchase, // From orders
+ Referral, // Friend referral
+ Activity, // App activity
+ Promotion, // Promotions
+ Admin // Admin manual grant
+}
+```
## Infrastructure
@@ -315,7 +591,7 @@ membership-service-net:
ports:
- "5002:8080"
labels:
- - "traefik.http.routers.membership.rule=PathPrefix(`/api/v1/members`)"
+ - "traefik.http.routers.membership.rule=PathPrefix(`/api/v1/members`) || PathPrefix(`/api/v1/levels`)"
```
### Kubernetes Deployment
@@ -349,26 +625,6 @@ spec:
httpGet:
path: /health/ready
port: 8080
- env:
- - name: IamService__BaseUrl
- value: "http://iam-service:8080"
-```
-
-## Scalability
-
-### Horizontal Scaling
-
-- Stateless service design
-- Database connection pooling
-- Read replicas for queries
-
-### Caching Strategy (Future)
-
-```
-┌─────────────┐ ┌─────────────┐ ┌─────────────┐
-│ Client │────▶│ Redis │────▶│ PostgreSQL │
-└─────────────┘ └─────────────┘ └─────────────┘
- Cache Layer Persistence
```
## Monitoring
@@ -383,15 +639,16 @@ spec:
### Metrics (Prometheus)
+- `membership_experience_added_total` - Total EXP added
+- `membership_level_ups_total` - Number of level ups
- `membership_requests_total` - Request count
- `membership_request_duration_seconds` - Latency
-- `membership_errors_total` - Error count
### Logging
Structured Serilog logging with:
- Request/Response logging
-- Domain event logging
+- Domain event logging (Level Up, EXP Added)
- Error tracking with stack traces
## Error Handling
@@ -401,9 +658,9 @@ Structured Serilog logging with:
```json
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.4",
- "title": "Not Found",
- "status": 404,
- "detail": "Member with ID 'xxx' was not found.",
+ "title": "Bad Request",
+ "status": 400,
+ "detail": "EXP points must be positive.",
"traceId": "00-1234567890abcdef-1234567890abcdef-00"
}
```
@@ -412,17 +669,19 @@ Structured Serilog logging with:
| HTTP Status | Scenario |
|-------------|----------|
-| 400 | Validation error |
+| 400 | Validation error (negative EXP, etc.) |
| 401 | Missing/invalid token |
-| 404 | Member not found |
-| 409 | Member already exists |
+| 403 | No permission (Admin endpoints) |
+| 404 | Member/level not found |
+| 409 | Level number already exists |
| 500 | Internal server error |
## Future Enhancements
-- [ ] Redis caching for member lookups
-- [ ] Event sourcing for audit trail
-- [ ] GraphQL endpoint
-- [ ] Batch member import/export
-- [ ] Member search with Elasticsearch
-- [ ] Webhook notifications on level changes
+- [ ] Redis caching for level definitions
+- [ ] Event sourcing for EXP history audit trail
+- [ ] Leaderboard with EXP ranking
+- [ ] Achievement/Badge system
+- [ ] EXP decay (reduce EXP if inactive)
+- [ ] Seasonal levels (reset by season)
+- [ ] Integration Events with RabbitMQ
diff --git a/services/membership-service-net/docs/vi/ARCHITECTURE.md b/services/membership-service-net/docs/vi/ARCHITECTURE.md
index 207fd4a9..c2e5b6f4 100644
--- a/services/membership-service-net/docs/vi/ARCHITECTURE.md
+++ b/services/membership-service-net/docs/vi/ARCHITECTURE.md
@@ -2,7 +2,7 @@
## Tổng Quan Hệ Thống
-Membership Service quản lý dữ liệu membership của user. Profile information được lưu tại IAM Service.
+Membership Service quản lý dữ liệu membership của user bao gồm **Level**, **Experience (EXP)**, và **Preferences**. Profile information được lưu tại IAM Service.
```mermaid
graph TB
@@ -17,8 +17,9 @@ graph TB
subgraph Services["Microservices"]
IAM["🔐 IAM Service
(User & Profile)"]
- MEMBER["👥 Membership Service
(Membership Data)"]
+ MEMBER["👥 Membership Service
(Level, EXP, Preferences)"]
STORAGE["📦 Storage Service"]
+ WALLET["💰 Wallet Service"]
end
subgraph Data["Data Layer"]
@@ -34,17 +35,20 @@ graph TB
IAM --> |"JWT Token"| MEMBER
MEMBER --> |"HTTP Client"| IAM
MEMBER --> PG
+ MEMBER -.-> |"Level Up Event"| WALLET
style IAM fill:#3b82f6,stroke:#1e40af,color:#fff
style MEMBER fill:#22c55e,stroke:#15803d,color:#fff
style STORAGE fill:#eab308,stroke:#a16207,color:#000
+ style WALLET fill:#a855f7,stroke:#7e22ce,color:#fff
style TRAEFIK fill:#6366f1,stroke:#4338ca,color:#fff
```
> [!IMPORTANT]
> **Phân chia trách nhiệm:**
> - **IAM Service**: Quản lý User identity và UserProfile (avatar, phone, address, DOB)
-> - **Membership Service**: Quản lý membership data (membership level, preferences, country code, gender)
+> - **Membership Service**: Quản lý membership data (level, experience, preferences, country code, gender)
+> - **Wallet Service**: Nhận event khi Level Up để xử lý rewards
## Các Lớp Clean Architecture
@@ -55,6 +59,7 @@ graph TB
├────────────────────────────────────────────────────────────┤
│ Lớp Domain (Core) │
│ Entities, Aggregates, Value Objects, Domain Events │
+│ Member Aggregate, LevelDefinition Aggregate │
├────────────────────────────────────────────────────────────┤
│ Lớp Infrastructure │
│ DbContext, Repositories, Entity Configurations │
@@ -64,7 +69,7 @@ graph TB
## Domain Model
-### Cấu Trúc Aggregate
+### Tổng Quan Aggregates
```mermaid
classDiagram
@@ -73,37 +78,115 @@ classDiagram
+Guid UserId
+string CountryCode
+string? Gender
- +int MembershipLevelId
- +MembershipLevel MembershipLevel
+ +int CurrentLevel
+ +int CurrentExp
+ +int TotalExpEarned
+string? Preferences
+bool IsDeleted
+DateTime CreatedAt
+DateTime UpdatedAt
+ +AddExperience(points, levelRules)
+UpdateGender()
+UpdateCountryCode()
+UpdatePreferences()
- +ChangeMembershipLevel()
+Delete()
+Restore()
}
- class MembershipLevel {
- <>
- +int Id
+ class LevelDefinition {
+ +Guid Id
+ +int LevelNumber
+string Name
- +Free = 1
- +Basic = 2
- +Premium = 3
+ +int RequiredExp
+ +string? Description
+ +string? IconUrl
+ +string? Benefits
+ +bool IsActive
+ +DateTime CreatedAt
+ +DateTime UpdatedAt
}
- Member --> MembershipLevel
+ Member ..> LevelDefinition : uses rules
style Member fill:#22c55e,stroke:#15803d,color:#fff
- style MembershipLevel fill:#3b82f6,stroke:#1e40af,color:#fff
+ style LevelDefinition fill:#3b82f6,stroke:#1e40af,color:#fff
+```
+
+### Member Aggregate (Rich Domain Model)
+
+```csharp
+public class Member : Entity, IAggregateRoot
+{
+ // Private fields để encapsulate state
+ private int _currentLevel;
+ private int _currentExp;
+ private int _totalExpEarned;
+
+ public int CurrentLevel => _currentLevel;
+ public int CurrentExp => _currentExp;
+ public int TotalExpEarned => _totalExpEarned;
+
+ ///
+ /// EN: Add experience and automatically level up if thresholds are met.
+ /// VI: Thêm exp và tự động lên level nếu đạt ngưỡng.
+ ///
+ public void AddExperience(int points, IReadOnlyList levelRules)
+ {
+ if (points <= 0)
+ throw new DomainException("EXP points must be positive");
+
+ int oldLevel = _currentLevel;
+ _currentExp += points;
+ _totalExpEarned += points;
+ _updatedAt = DateTime.UtcNow;
+
+ // Business logic: Calculate new level
+ var newLevel = CalculateLevel(_currentExp, levelRules);
+
+ if (newLevel > oldLevel)
+ {
+ _currentLevel = newLevel;
+ // Raise domain event for side effects
+ AddDomainEvent(new MemberLevelUpDomainEvent(this, oldLevel, newLevel));
+ }
+
+ AddDomainEvent(new MemberExperienceAddedDomainEvent(this, points));
+ }
+
+ private static int CalculateLevel(int exp, IReadOnlyList rules)
+ {
+ return rules
+ .Where(r => r.IsActive && r.RequiredExp <= exp)
+ .OrderByDescending(r => r.LevelNumber)
+ .FirstOrDefault()?.LevelNumber ?? 1;
+ }
+}
```
> [!NOTE]
-> Member entity không lưu trữ profile fields (phone, avatar, address, DOB). Những fields này được quản lý bởi IAM Service's UserProfile.
+> **Rich Domain Model**: Logic nghiệp vụ về EXP và Level được đóng gói trong Member entity, không phải ở Service Layer. Điều này tránh mô hình "Anemic Domain Model".
+
+### LevelDefinition Aggregate (Customizable Levels)
+
+```csharp
+public class LevelDefinition : Entity, IAggregateRoot
+{
+ public int LevelNumber { get; private set; } // 1, 2, 3...
+ public string Name { get; private set; } // "Bronze", "Silver", "Gold"
+ public int RequiredExp { get; private set; } // 0, 100, 300, 600...
+ public string? Description { get; private set; }
+ public string? IconUrl { get; private set; }
+ public string? Benefits { get; private set; } // JSON benefits
+ public bool IsActive { get; private set; }
+
+ public void UpdateRequiredExp(int newRequiredExp)
+ {
+ if (newRequiredExp < 0)
+ throw new DomainException("Required EXP cannot be negative");
+ RequiredExp = newRequiredExp;
+ }
+}
+```
### Domain Events
@@ -111,53 +194,102 @@ classDiagram
|-------|---------|----------|
| `MemberCreatedDomainEvent` | Tạo thành viên mới | Email chào mừng, analytics |
| `MemberUpdatedDomainEvent` | Cập nhật hồ sơ | Đồng bộ với services khác |
-| `MembershipLevelChangedDomainEvent` | Thay đổi cấp độ | Thanh toán, cập nhật quyền lợi |
+| `MemberExperienceAddedDomainEvent` | Nhận EXP | Logging, analytics |
+| `MemberLevelUpDomainEvent` | Lên level | **Rewards, notifications, unlock features** |
+
+```mermaid
+sequenceDiagram
+ participant Client
+ participant Handler
+ participant Member
+ participant EventDispatcher
+ participant WalletService
+
+ Client->>Handler: AddExperienceCommand
+ Handler->>Member: AddExperience(100, rules)
+ Member->>Member: _currentExp += 100
+ Member->>Member: CalculateLevel()
+
+ alt Level Up
+ Member->>Member: AddDomainEvent(LevelUpEvent)
+ end
+
+ Handler->>EventDispatcher: SaveEntitiesAsync()
+ EventDispatcher->>WalletService: Publish LevelUpEvent
+ WalletService-->>WalletService: Grant Rewards
+```
## CQRS Pattern
+### Write Side (Commands)
+
```mermaid
flowchart LR
- subgraph Commands["✏️ Write Side"]
- CMD1["CreateMemberCommand"]
- CMD2["UpdateMemberProfileCommand"]
- CMD3["ChangeMembershipLevelCommand"]
+ subgraph Commands["✏️ Commands"]
+ CMD1["AddExperienceCommand"]
+ CMD2["CreateMemberCommand"]
+ CMD3["UpdateMemberProfileCommand"]
+ CMD4["CreateLevelDefinitionCommand"]
end
subgraph Handlers["⚙️ Command Handlers"]
- H1["CreateMemberHandler"]
- H2["UpdateProfileHandler"]
- H3["ChangeLevelHandler"]
+ H1["AddExperienceHandler"]
+ H2["CreateMemberHandler"]
+ H3["UpdateProfileHandler"]
+ H4["CreateLevelHandler"]
end
- subgraph Queries["📖 Read Side"]
- Q1["GetMemberByIdQuery"]
- Q2["GetMembersQuery"]
- end
-
- subgraph QueryHandlers["🔍 Query Handlers"]
- QH1["GetMemberByIdHandler"]
- QH2["GetMembersHandler"]
+ subgraph Domain["🏛️ Domain Model"]
+ DM["Rich Domain Model
EF Core"]
end
DB[("🐘 Database")]
- CMD1 --> H1 --> DB
- CMD2 --> H2 --> DB
- CMD3 --> H3 --> DB
-
- Q1 --> QH1 --> DB
- Q2 --> QH2 --> DB
+ CMD1 --> H1 --> DM --> DB
+ CMD2 --> H2 --> DM
+ CMD3 --> H3 --> DM
+ CMD4 --> H4 --> DM
style CMD1 fill:#ef4444,stroke:#b91c1c,color:#fff
style CMD2 fill:#ef4444,stroke:#b91c1c,color:#fff
style CMD3 fill:#ef4444,stroke:#b91c1c,color:#fff
+ style CMD4 fill:#ef4444,stroke:#b91c1c,color:#fff
+```
+
+### Read Side (Queries)
+
+```mermaid
+flowchart LR
+ subgraph Queries["📖 Queries"]
+ Q1["GetMemberByIdQuery"]
+ Q2["GetMemberProgressQuery"]
+ Q3["GetLevelDefinitionsQuery"]
+ end
+
+ subgraph Handlers["🔍 Query Handlers"]
+ QH1["GetMemberByIdHandler"]
+ QH2["GetMemberProgressHandler"]
+ QH3["GetLevelDefinitionsHandler"]
+ end
+
+ subgraph ReadModel["📊 Read Model"]
+ RM["Dapper / Raw SQL"]
+ end
+
+ DB[("🐘 Database")]
+
+ Q1 --> QH1 --> RM --> DB
+ Q2 --> QH2 --> RM
+ Q3 --> QH3 --> RM
+
style Q1 fill:#22c55e,stroke:#15803d,color:#fff
style Q2 fill:#22c55e,stroke:#15803d,color:#fff
+ style Q3 fill:#22c55e,stroke:#15803d,color:#fff
```
## Luồng Request
-### Luồng Tạo Thành Viên
+### Luồng Add Experience
```mermaid
sequenceDiagram
@@ -165,23 +297,58 @@ sequenceDiagram
participant Controller
participant MediatR
participant Handler
- participant Repository
+ participant MemberRepo
+ participant LevelRepo
participant DB
- Client->>Controller: POST /api/v1/members
- Controller->>MediatR: Send(CreateMemberCommand)
+ Client->>Controller: POST /api/v1/members/{id}/experience
+ Note over Client,Controller: { "points": 100, "source": "purchase" }
+
+ Controller->>MediatR: Send(AddExperienceCommand)
MediatR->>Handler: Handle(command)
- Handler->>Repository: ExistsByUserIdAsync(userId)
- Repository->>DB: SELECT EXISTS
- DB-->>Repository: false
- Handler->>Repository: Add(member)
- Handler->>Repository: SaveEntitiesAsync()
- Repository->>DB: INSERT + Domain Events
- DB-->>Repository: OK
- Repository-->>Handler: member
- Handler-->>MediatR: CreateMemberResult
- MediatR-->>Controller: result
- Controller-->>Client: 201 Created
+
+ Handler->>MemberRepo: GetAsync(memberId)
+ MemberRepo->>DB: SELECT member
+ DB-->>MemberRepo: member data
+
+ Handler->>LevelRepo: GetAllActiveAsync()
+ LevelRepo->>DB: SELECT level_definitions
+ DB-->>LevelRepo: level rules
+
+ Handler->>Handler: member.AddExperience(100, rules)
+ Note over Handler: Business logic in Domain
+
+ Handler->>MemberRepo: UnitOfWork.SaveEntitiesAsync()
+ MemberRepo->>DB: UPDATE + Dispatch Domain Events
+
+ DB-->>Handler: OK
+ Handler-->>Controller: AddExperienceResult
+ Controller-->>Client: 200 OK
+ Note over Client,Controller: { "currentExp": 150, "currentLevel": 2, "leveledUp": true }
+```
+
+### Luồng Get Member Progress
+
+```mermaid
+sequenceDiagram
+ participant Client
+ participant Controller
+ participant Handler
+ participant Dapper
+ participant DB
+
+ Client->>Controller: GET /api/v1/members/{id}/progress
+ Controller->>Handler: GetMemberProgressQuery
+ Handler->>Dapper: Raw SQL Query
+
+ Note over Dapper,DB: SELECT m.current_exp, m.current_level,
l.name, l.required_exp, next_level.required_exp
FROM members m
JOIN level_definitions l ON ...
+
+ Dapper->>DB: Execute
+ DB-->>Dapper: Result
+ Dapper-->>Handler: MemberProgressDto
+ Handler-->>Controller: Result
+ Controller-->>Client: 200 OK
+ Note over Client,Controller: { "currentLevel": 2, "levelName": "Silver",
"currentExp": 150, "expToNextLevel": 150,
"progressPercent": 50 }
```
## Database Schema
@@ -192,35 +359,107 @@ erDiagram
uuid id PK "Same as IAM UserId"
varchar country_code "ISO 3166-1 alpha-2"
varchar gender "male/female/other"
- int membership_level_id FK
+ int current_level "Current member level"
+ int current_exp "Current experience points"
+ int total_exp_earned "Total EXP ever earned"
jsonb preferences "User preferences"
boolean is_deleted "Soft delete flag"
timestamp created_at
timestamp updated_at
}
- MEMBERSHIP_LEVELS {
- int id PK
- varchar name
+ LEVEL_DEFINITIONS {
+ uuid id PK
+ int level_number UK "Level 1, 2, 3..."
+ varchar name "Bronze, Silver, Gold..."
+ int required_exp "EXP needed for this level"
+ text description
+ varchar icon_url
+ jsonb benefits "Level benefits/rewards"
+ boolean is_active
+ timestamp created_at
+ timestamp updated_at
}
- MEMBERS }o--|| MEMBERSHIP_LEVELS : has
+ MEMBERS }o..|| LEVEL_DEFINITIONS : "current_level references"
+```
+
+### SQL Schema
+
+```sql
+-- Bảng Level Definitions (Admin configurable)
+CREATE TABLE level_definitions (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ level_number INT NOT NULL UNIQUE,
+ name VARCHAR(100) NOT NULL,
+ required_exp INT NOT NULL DEFAULT 0,
+ description TEXT,
+ icon_url VARCHAR(500),
+ benefits JSONB,
+ is_active BOOLEAN NOT NULL DEFAULT TRUE,
+ created_at TIMESTAMP NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMP NOT NULL DEFAULT NOW()
+);
+
+-- Bảng Members
+CREATE TABLE members (
+ id UUID PRIMARY KEY,
+ country_code VARCHAR(2) NOT NULL DEFAULT 'VN',
+ gender VARCHAR(10),
+ current_level INT NOT NULL DEFAULT 1,
+ current_exp INT NOT NULL DEFAULT 0,
+ total_exp_earned INT NOT NULL DEFAULT 0,
+ preferences JSONB,
+ is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
+ created_at TIMESTAMP NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMP NOT NULL DEFAULT NOW()
+);
+
+-- Indexes
+CREATE INDEX ix_members_current_level ON members(current_level);
+CREATE INDEX ix_members_current_exp ON members(current_exp);
+CREATE INDEX ix_level_definitions_level_number ON level_definitions(level_number);
+CREATE INDEX ix_level_definitions_is_active ON level_definitions(is_active);
+
+-- Default Level Definitions
+INSERT INTO level_definitions (level_number, name, required_exp, description, is_active)
+VALUES
+ (1, 'Bronze', 0, 'Starting level', true),
+ (2, 'Silver', 100, 'Reach 100 EXP', true),
+ (3, 'Gold', 300, 'Reach 300 EXP', true),
+ (4, 'Platinum', 600, 'Reach 600 EXP', true),
+ (5, 'Diamond', 1000, 'Reach 1000 EXP', true);
```
## Thiết Kế API
### RESTful Endpoints
+#### Member Endpoints
+
```
GET /api/v1/members # Danh sách (phân trang, search)
GET /api/v1/members/{id} # Lấy theo ID
GET /api/v1/members/me # Lấy user hiện tại
+GET /api/v1/members/{id}/progress # Lấy tiến trình level
POST /api/v1/members # Tạo mới
+POST /api/v1/members/{id}/experience # Thêm EXP
PUT /api/v1/members/{id} # Cập nhật hồ sơ
-PUT /api/v1/members/{id}/level # Thay đổi cấp độ
```
-### Định Dạng Response
+#### Level Endpoints (Admin)
+
+```
+GET /api/v1/levels # Danh sách level definitions
+GET /api/v1/levels/{id} # Lấy level definition
+POST /api/v1/levels # Tạo level definition (Admin)
+PUT /api/v1/levels/{id} # Cập nhật level definition (Admin)
+DELETE /api/v1/levels/{id} # Deactivate level (Admin)
+```
+
+### Response Formats
+
+#### Member Response
```json
{
@@ -228,76 +467,113 @@ PUT /api/v1/members/{id}/level # Thay đổi cấp độ
"userId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"countryCode": "VN",
"gender": "male",
- "preferences": "{\"theme\": \"dark\", \"language\": \"vi\"}",
- "membershipLevel": {
- "id": 1,
- "name": "Free"
- },
+ "currentLevel": 2,
+ "currentExp": 150,
+ "totalExpEarned": 150,
+ "preferences": "{\"theme\": \"dark\"}",
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T10:30:00Z"
}
```
+#### Member Progress Response
+
+```json
+{
+ "memberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
+ "currentLevel": 2,
+ "levelName": "Silver",
+ "levelIconUrl": "/icons/silver.png",
+ "currentExp": 150,
+ "expToNextLevel": 150,
+ "nextLevelRequiredExp": 300,
+ "progressPercent": 50.0
+}
+```
+
+#### Add Experience Response
+
+```json
+{
+ "currentExp": 250,
+ "currentLevel": 2,
+ "totalExpEarned": 250,
+ "leveledUp": false,
+ "newLevel": null
+}
+```
+
+#### Level Definition Response
+
+```json
+{
+ "id": "a1b2c3d4-...",
+ "levelNumber": 3,
+ "name": "Gold",
+ "requiredExp": 300,
+ "description": "Reach 300 EXP to unlock Gold status",
+ "iconUrl": "/icons/gold.png",
+ "benefits": {
+ "discountPercent": 10,
+ "prioritySupport": true,
+ "freeShipping": false
+ },
+ "isActive": true
+}
+```
+
## Inter-Service Communication
### IAM Service Client
-Membership Service giao tiếp với IAM Service thông qua HTTP Client:
+Membership Service giao tiếp với IAM Service thông qua HTTP Client để validate users.
+
+### Integration Events (Planned)
+
+Khi member lên level, publish Integration Event để các services khác xử lý:
```mermaid
-sequenceDiagram
- participant Membership
- participant IamClient["IIamServiceClient"]
- participant Cache["In-Memory Cache"]
- participant IAM["IAM Service"]
+flowchart LR
+ MEMBER["👥 Membership Service"]
+ BUS["📬 Event Bus
(RabbitMQ)"]
+ WALLET["💰 Wallet Service"]
+ NOTIF["🔔 Notification Service"]
- Membership->>IamClient: ValidateUserAsync(token)
- IamClient->>Cache: TryGetFromCache(cacheKey)
+ MEMBER -->|MemberLevelUpIntegrationEvent| BUS
+ BUS -->|Subscribe| WALLET
+ BUS -->|Subscribe| NOTIF
- alt Cache Hit
- Cache-->>IamClient: cached user info
- else Cache Miss
- IamClient->>IAM: GET /api/v1/users/me
- IAM-->>IamClient: user info
- IamClient->>Cache: AddToCache(user, 5min)
- end
-
- IamClient-->>Membership: IamUserInfo
+ WALLET -->|Grant Rewards| WALLET
+ NOTIF -->|Send Notification| NOTIF
+
+ style MEMBER fill:#22c55e,stroke:#15803d,color:#fff
+ style BUS fill:#ef4444,stroke:#b91c1c,color:#fff
+ style WALLET fill:#a855f7,stroke:#7e22ce,color:#fff
+ style NOTIF fill:#eab308,stroke:#a16207,color:#000
```
-### IAM Client Features
-
-| Feature | Mô tả |
-|---------|-------|
-| User Validation | Xác thực token và lấy user info |
-| Role/Permission Check | Kiểm tra roles và permissions của user |
-| Health Check | Kiểm tra IAM Service availability |
-| Caching | In-memory cache với TTL 5 phút |
-
## Bảo Mật
-### Luồng Xác Thực
-
-```mermaid
-sequenceDiagram
- participant Client
- participant Traefik
- participant Membership
- participant IAM
-
- Client->>IAM: Login
- IAM-->>Client: JWT Token
- Client->>Traefik: Request + Bearer Token
- Traefik->>Membership: Forward request
- Membership->>Membership: Validate JWT
- Membership-->>Client: Response
-```
-
### Authorization
- Tất cả endpoints yêu cầu xác thực JWT Bearer
- Token được xác thực với IAM Service authority
- UserId được trích xuất từ JWT claims (`sub` hoặc `id`)
+- **Admin endpoints** (`/api/v1/levels` POST/PUT/DELETE) yêu cầu role `admin`
+
+### Experience Source Validation
+
+```csharp
+// Validate experience source để tránh abuse
+public enum ExperienceSource
+{
+ Purchase, // Từ đơn hàng
+ Referral, // Giới thiệu bạn bè
+ Activity, // Hoạt động trên app
+ Promotion, // Khuyến mãi
+ Admin // Admin manual grant
+}
+```
## Hạ Tầng
@@ -315,7 +591,7 @@ membership-service-net:
ports:
- "5002:8080"
labels:
- - "traefik.http.routers.membership.rule=PathPrefix(`/api/v1/members`)"
+ - "traefik.http.routers.membership.rule=PathPrefix(`/api/v1/members`) || PathPrefix(`/api/v1/levels`)"
```
### Kubernetes Deployment
@@ -349,26 +625,6 @@ spec:
httpGet:
path: /health/ready
port: 8080
- env:
- - name: IamService__BaseUrl
- value: "http://iam-service:8080"
-```
-
-## Khả Năng Mở Rộng
-
-### Mở Rộng Theo Chiều Ngang
-
-- Thiết kế service stateless
-- Connection pooling cho database
-- Read replicas cho queries
-
-### Chiến Lược Caching (Tương Lai)
-
-```
-┌─────────────┐ ┌─────────────┐ ┌─────────────┐
-│ Client │────▶│ Redis │────▶│ PostgreSQL │
-└─────────────┘ └─────────────┘ └─────────────┘
- Cache Layer Persistence
```
## Giám Sát
@@ -383,27 +639,28 @@ spec:
### Metrics (Prometheus)
+- `membership_experience_added_total` - Tổng EXP đã cộng
+- `membership_level_ups_total` - Số lần lên level
- `membership_requests_total` - Số lượng request
- `membership_request_duration_seconds` - Độ trễ
-- `membership_errors_total` - Số lỗi
### Logging
Structured Serilog logging với:
- Request/Response logging
-- Domain event logging
+- Domain event logging (Level Up, EXP Added)
- Error tracking với stack traces
## Xử Lý Lỗi
-### Định Dạng Error Response
+### Error Response Format
```json
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.4",
- "title": "Not Found",
- "status": 404,
- "detail": "Không tìm thấy thành viên với ID 'xxx'.",
+ "title": "Bad Request",
+ "status": 400,
+ "detail": "EXP points must be positive.",
"traceId": "00-1234567890abcdef-1234567890abcdef-00"
}
```
@@ -412,17 +669,19 @@ Structured Serilog logging với:
| HTTP Status | Tình huống |
|-------------|------------|
-| 400 | Lỗi validation |
+| 400 | Validation error (EXP âm, etc.) |
| 401 | Thiếu/sai token |
-| 404 | Không tìm thấy thành viên |
-| 409 | Thành viên đã tồn tại |
+| 403 | Không có quyền (Admin endpoints) |
+| 404 | Không tìm thấy member/level |
+| 409 | Level number đã tồn tại |
| 500 | Lỗi server nội bộ |
## Cải Tiến Tương Lai
-- [ ] Redis caching cho member lookups
-- [ ] Event sourcing cho audit trail
-- [ ] GraphQL endpoint
-- [ ] Batch member import/export
-- [ ] Member search với Elasticsearch
-- [ ] Webhook notifications khi level thay đổi
+- [ ] Redis caching cho level definitions
+- [ ] Event sourcing cho EXP history audit trail
+- [ ] Leaderboard với ranking theo EXP
+- [ ] Achievement/Badge system
+- [ ] EXP decay (giảm EXP nếu không hoạt động)
+- [ ] Seasonal levels (reset theo mùa)
+- [ ] Integration Events với RabbitMQ