diff --git a/docs/en/test-mermaid.md b/docs/en/test-mermaid.md
deleted file mode 100644
index e5c82887..00000000
--- a/docs/en/test-mermaid.md
+++ /dev/null
@@ -1,12 +0,0 @@
-# Mermaid Test
-
-This is a test diagram.
-
-```mermaid
-graph TD;
- A[Start] --> B{Is it working?};
- B -- Yes --> C[Great!];
- B -- No --> D[Debug more];
-```
-
-End of test.
diff --git a/services/chat-service-net/README.md b/services/chat-service-net/README.md
new file mode 100644
index 00000000..c6995c67
--- /dev/null
+++ b/services/chat-service-net/README.md
@@ -0,0 +1,56 @@
+# Chat Service
+
+> Real-time chat service with End-to-End Encryption (E2EE) for GoodGo platform.
+
+## Documentation / Tài Liệu
+
+> **EN**: [English Documentation](docs/en/README.md)
+> **VI**: [Tài liệu Tiếng Việt](docs/vi/README.md)
+
+## Quick Links
+
+| English | Vietnamese |
+|---------|------------|
+| [Architecture](docs/en/ARCHITECTURE.md) | [Kiến trúc](docs/vi/ARCHITECTURE.md) |
+| [Quick Start](docs/en/README.md#quick-start) | [Bắt Đầu Nhanh](docs/vi/README.md#bắt-đầu-nhanh) |
+| [API Reference](docs/en/README.md#api-endpoints) | [API Reference](docs/vi/README.md#api-endpoints) |
+
+## Tech Stack
+
+- **.NET 10** - Core framework
+- **ASP.NET Core SignalR** - Real-time communication
+- **PostgreSQL 16+** - Message persistence
+- **Redis 7+** - Backplane & caching
+- **E2EE with X3DH** - End-to-end encryption
+
+## Key Features
+
+- 🔒 **End-to-End Encryption** - X3DH key exchange, AES-256-GCM
+- 💬 **Real-time Chat** - SignalR with WebSocket/SSE/Long Polling
+- 🤖 **AI Integration** - Smart chatbot with streaming responses
+- 📱 **Multi-device** - User mapping across devices
+- 🚀 **High Performance** - MessagePack protocol
+
+## Development
+
+```bash
+# Restore dependencies
+dotnet restore
+
+# Build
+dotnet build
+
+# Run
+dotnet run --project src/ChatService.API
+```
+
+## Docker
+
+```bash
+docker build -t chatservice:latest .
+docker run -p 5000:8080 --env-file .env chatservice:latest
+```
+
+## License
+
+Proprietary - GoodGo Platform
diff --git a/services/chat-service-net/docs/en/ARCHITECTURE.md b/services/chat-service-net/docs/en/ARCHITECTURE.md
index 62e47eb0..044db62c 100644
--- a/services/chat-service-net/docs/en/ARCHITECTURE.md
+++ b/services/chat-service-net/docs/en/ARCHITECTURE.md
@@ -1,6 +1,6 @@
# Chat Service Architecture Documentation
-> Detailed architecture for Chat Service with SignalR, scalability patterns, and AI integration.
+> Detailed architecture for Chat Service with E2EE, SignalR, scalability patterns, and AI integration.
## Architecture Overview
@@ -43,12 +43,138 @@ graph TB
CS1 & CS2 & CS3 --> RC
CS1 & CS2 & CS3 --> AI
- style LB fill:#3498db,stroke:#2980b9,color:#fff
- style RD fill:#e74c3c,stroke:#c0392b,color:#fff
- style PG fill:#27ae60,stroke:#1e8449,color:#fff
- style AI fill:#9b59b6,stroke:#7d3c98,color:#fff
+ style LB fill:#3498DB,color:#ECF0F1,stroke:#2980B9,stroke-width:3px
+ style RD fill:#C0392B,color:#ECF0F1,stroke:#A93226,stroke-width:2px
+ style PG fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px
+ style AI fill:#8E44AD,color:#ECF0F1,stroke:#7D3C98,stroke-width:2px
```
+## End-to-End Encryption (E2EE)
+
+### X3DH Key Exchange Protocol
+
+The service implements the Extended Triple Diffie-Hellman (X3DH) protocol for establishing encrypted sessions:
+
+```mermaid
+sequenceDiagram
+ participant Alice as Alice (Sender)
+ participant Server as Chat Server
+ participant Bob as Bob (Receiver)
+
+ Note over Bob,Server: Bob registers keys
+ Bob->>Server: Register Key Bundle
(Identity, SignedPreKey, OneTimePreKeys)
+ Server->>Server: Store public keys only
+
+ Note over Alice,Server: Alice initiates session
+ Alice->>Server: Request Bob's Key Bundle
+ Server->>Alice: {IdentityKey, SignedPreKey, OneTimePreKey}
+ Alice->>Alice: X3DH → Session Key
+ Alice->>Alice: AES-256-GCM encrypt message
+ Alice->>Server: Send encrypted message
+ Server->>Bob: Deliver encrypted message
+ Bob->>Bob: X3DH → Session Key
+ Bob->>Bob: AES-256-GCM decrypt message
+
+ style Server fill:#2C3E50,color:#ECF0F1,stroke:#34495E,stroke-width:3px
+```
+
+### Key Components
+
+| Component | Description | Storage |
+|-----------|-------------|---------|
+| **Identity Key** | Long-term Curve25519 key pair | Private: Client only |
+| **Signed Pre-Key** | Rotated every 30 days | Public: Server |
+| **One-Time Pre-Keys** | Consumed per session | Public: Server (deleted after use) |
+| **Session Key** | Derived via X3DH | Never stored |
+
+### Security Properties
+
+- ✅ **Forward Secrecy**: Compromised keys don't reveal past messages
+- ✅ **Zero-Knowledge Server**: Server cannot decrypt messages
+- ✅ **Deniability**: No cryptographic proof of authorship
+
+## Domain Model
+
+### Aggregate Roots
+
+```mermaid
+erDiagram
+ Conversation ||--o{ Message : contains
+ Conversation ||--o{ ConversationParticipant : has
+ ChatUser ||--o{ Conversation : participates
+ ChatUser ||--|| UserKeyBundle : has
+ ChatUser ||--o{ OneTimePreKey : owns
+
+ Conversation {
+ uuid Id PK
+ string Name
+ string AvatarUrl
+ enum Type "Direct|Group"
+ uuid CreatorId
+ datetime CreatedAt
+ datetime LastActivityAt
+ }
+
+ Message {
+ uuid Id PK
+ uuid ConversationId FK
+ uuid SenderId FK
+ string EncryptedContent
+ string Nonce
+ string AuthTag
+ enum Type "Text|Image|File|System"
+ enum Status "Sent|Delivered|Read|Failed"
+ datetime CreatedAt
+ }
+
+ ConversationParticipant {
+ uuid Id PK
+ uuid ConversationId FK
+ uuid ChatUserId FK
+ enum Role "Owner|Admin|Member"
+ datetime JoinedAt
+ datetime LastReadAt
+ }
+
+ ChatUser {
+ uuid Id PK
+ string IdentityUserId
+ string DisplayName
+ string AvatarUrl
+ enum Status "Online|Away|Offline"
+ datetime LastSeenAt
+ }
+
+ UserKeyBundle {
+ string IdentityPublicKey
+ string SignedPreKey
+ string SignedPreKeySignature
+ datetime SignedPreKeyTimestamp
+ }
+
+ OneTimePreKey {
+ uuid Id PK
+ int KeyId
+ string PublicKey
+ bool IsUsed
+ datetime CreatedAt
+ }
+```
+
+### Domain Events
+
+| Event | Trigger | Handler |
+|-------|---------|---------|
+| `ConversationCreatedDomainEvent` | New conversation created | Notify participants |
+| `MessageSentDomainEvent` | Message sent | Update last_activity, broadcast |
+| `MessageDeliveredDomainEvent` | Message delivered | Notify sender |
+| `MessageReadDomainEvent` | Message read | Notify sender |
+| `UserJoinedRoomDomainEvent` | User joins room | Notify members |
+| `UserLeftRoomDomainEvent` | User leaves room | Notify members |
+| `TypingDomainEvent` | User typing | Broadcast to room |
+| `ChatUserCreatedDomainEvent` | New chat user | Initialize presence |
+| `UserKeyBundleUpdatedDomainEvent` | Keys updated | Invalidate cached keys |
+
## SignalR Hub Architecture
### Connection Lifecycle
@@ -63,74 +189,47 @@ sequenceDiagram
Client->>Hub: Connect (JWT Token)
Hub->>Hub: OnConnectedAsync()
- Hub->>DB: Load user rooms
- Hub->>Groups: AddToGroupAsync(roomId)
+ Hub->>DB: Load user conversations
+ Hub->>Groups: AddToGroupAsync(conversationId)
Hub->>Redis: Publish(UserOnline)
Hub-->>Client: Connected
Note over Client,Hub: User is now online
- Client->>Hub: SendMessage(roomId, content)
- Hub->>DB: Save message
+ Client->>Hub: SendMessage(conversationId, encryptedContent)
+ Hub->>DB: Save encrypted message
Hub->>Redis: Publish(NewMessage)
Redis-->>Hub: Broadcast to all instances
- Hub->>Groups: Clients.Group(roomId)
+ Hub->>Groups: Clients.Group(conversationId)
Hub-->>Client: ReceiveMessage
+
+ style Hub fill:#2C3E50,color:#ECF0F1,stroke:#34495E,stroke-width:3px
+ style Redis fill:#C0392B,color:#ECF0F1,stroke:#A93226,stroke-width:2px
+ style DB fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px
```
-### Hub Implementation
+### Hub Methods
```csharp
-public class ChatHub : Hub
+public class ChatHub : Hub
{
- private readonly IChatRoomRepository _roomRepository;
- private readonly IMessageRepository _messageRepository;
- private readonly IAIService _aiService;
+ // Connection management
+ public override Task OnConnectedAsync();
+ public override Task OnDisconnectedAsync(Exception? exception);
- public async Task JoinRoom(Guid roomId)
- {
- var userId = Context.UserIdentifier;
-
- // Add to SignalR Group
- await Groups.AddToGroupAsync(Context.ConnectionId, roomId.ToString());
-
- // Notify other members
- await Clients.Group(roomId.ToString())
- .UserJoined(userId, roomId);
-
- // Persist state to DB
- await _roomRepository.AddParticipantAsync(roomId, userId);
- }
+ // Room management
+ public Task JoinRoom(Guid conversationId);
+ public Task LeaveRoom(Guid conversationId);
- public async Task SendMessage(Guid roomId, string content)
- {
- var message = new Message(roomId, Context.UserIdentifier, content);
- await _messageRepository.AddAsync(message);
-
- // Broadcast message
- await Clients.Group(roomId.ToString())
- .ReceiveMessage(message.ToDto());
-
- // Check AI trigger
- if (content.StartsWith("@gpt "))
- {
- await StreamAIResponse(roomId, content[5..]);
- }
- }
+ // Messaging
+ public Task SendMessage(Guid conversationId, string encryptedContent,
+ string nonce, string? authTag);
+ public Task SendTypingIndicator(Guid conversationId, bool isTyping);
+ public Task MarkMessageRead(Guid conversationId, Guid messageId);
- public async IAsyncEnumerable StreamAIResponse(
- Guid roomId,
- string prompt,
- [EnumeratorCancellation] CancellationToken ct = default)
- {
- // Get chat history for context
- var history = await _messageRepository.GetHistoryAsync(roomId, 20);
-
- await foreach (var chunk in _aiService.StreamAsync(prompt, history, ct))
- {
- yield return chunk;
- }
- }
+ // AI Integration
+ public IAsyncEnumerable StreamAIResponse(Guid conversationId,
+ string prompt);
}
```
@@ -156,7 +255,9 @@ graph LR
CH -->|Subscribe| H2
H2 -->|Deliver| C2
- style CH fill:#e74c3c,stroke:#c0392b,color:#fff
+ style CH fill:#C0392B,color:#ECF0F1,stroke:#A93226,stroke-width:2px
+ style H1 fill:#2C3E50,color:#ECF0F1,stroke:#34495E,stroke-width:2px
+ style H2 fill:#2C3E50,color:#ECF0F1,stroke:#34495E,stroke-width:2px
```
### Scaling Options
@@ -167,86 +268,6 @@ graph LR
| **Azure SignalR** | Serverless, no sticky sessions | Vendor lock-in, cost |
| **Sticky Sessions** | Simplest | Imperfect for failover |
-```csharp
-// Redis Backplane Configuration
-builder.Services.AddSignalR()
- .AddStackExchangeRedis(options =>
- {
- options.Configuration.ChannelPrefix =
- RedisChannel.Literal("ChatService");
- options.Configuration.AbortOnConnectFail = false;
- });
-
-// Azure SignalR Service
-builder.Services.AddSignalR()
- .AddAzureSignalR(options =>
- {
- options.ConnectionString = azureSignalRConnectionString;
- options.ServerStickyMode = ServerStickyMode.Required;
- });
-```
-
-## User & Group Management
-
-### User Mapping
-
-```mermaid
-graph TB
- subgraph "User Mapping"
- U1[User ID: user-123]
- C1[Connection 1
Phone]
- C2[Connection 2
Laptop]
- C3[Connection 3
Tablet]
- end
-
- U1 --> C1 & C2 & C3
-
- style U1 fill:#3498db,stroke:#2980b9,color:#fff
-```
-
-### IUserIdProvider
-
-```csharp
-public class ClaimsUserIdProvider : IUserIdProvider
-{
- public string? GetUserId(HubConnectionContext connection)
- {
- // Get User ID from JWT Claims
- return connection.User?
- .FindFirst(ClaimTypes.NameIdentifier)?.Value;
- }
-}
-
-// Registration
-builder.Services.AddSingleton();
-```
-
-### Group State Management
-
-```csharp
-// Problem: SignalR Groups don't persist on restart
-// Solution: Store in database
-
-public class GroupStateService
-{
- private readonly IDistributedCache _cache;
- private readonly IChatRoomRepository _repository;
-
- public async Task RestoreUserGroups(string userId, string connectionId)
- {
- var rooms = await _repository.GetUserRoomsAsync(userId);
-
- foreach (var room in rooms)
- {
- await _hubContext.Groups.AddToGroupAsync(
- connectionId,
- room.Id.ToString()
- );
- }
- }
-}
-```
-
## AI Integration Flow
```mermaid
@@ -270,74 +291,10 @@ sequenceDiagram
Hub->>DB: Save AI response
Hub-->>User: AIResponseComplete
-```
-
-## Domain Model
-
-### Aggregate Roots
-
-```mermaid
-erDiagram
- ChatRoom ||--o{ Message : contains
- ChatRoom ||--o{ Participant : has
- ChatRoom {
- uuid id PK
- string name
- enum type
- uuid owner_id
- timestamp created_at
- }
- Message {
- uuid id PK
- uuid room_id FK
- uuid sender_id
- string content
- enum type
- timestamp sent_at
- }
-
- Participant {
- uuid id PK
- uuid room_id FK
- uuid user_id
- enum role
- timestamp joined_at
- timestamp last_read
- }
-```
-
-### Domain Events
-
-| Event | Trigger | Handler |
-|-------|---------|---------|
-| `MessageSentEvent` | New message | Update last_activity, broadcast |
-| `UserJoinedRoomEvent` | User joins room | Notify members |
-| `UserLeftRoomEvent` | User leaves room | Notify members |
-| `RoomCreatedEvent` | Create new room | Add creator as admin |
-
-## MessagePack Protocol
-
-### JSON vs MessagePack Comparison
-
-| Metric | JSON | MessagePack |
-|--------|------|-------------|
-| Size | 100% | ~50-70% |
-| Parse time | 1x | 0.5-0.8x |
-| Type safety | Weak | Strong |
-
-### Configuration
-
-```csharp
-// Server
-builder.Services.AddSignalR()
- .AddMessagePackProtocol(options =>
- {
- options.SerializerOptions =
- MessagePackSerializerOptions.Standard
- .WithSecurity(MessagePackSecurity.UntrustedData)
- .WithCompression(MessagePackCompression.Lz4BlockArray);
- });
+ style Hub fill:#2C3E50,color:#ECF0F1,stroke:#34495E,stroke-width:3px
+ style AI fill:#8E44AD,color:#ECF0F1,stroke:#7D3C98,stroke-width:2px
+ style OpenAI fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px
```
## Resiliency Patterns
@@ -413,32 +370,6 @@ spec:
targetPort: 8080
```
-### Ingress with WebSocket Support
-
-```yaml
-apiVersion: networking.k8s.io/v1
-kind: Ingress
-metadata:
- name: chatservice
- annotations:
- nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
- nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
- nginx.ingress.kubernetes.io/upstream-hash-by: "$request_uri"
- nginx.ingress.kubernetes.io/affinity: "cookie"
-spec:
- rules:
- - host: chat.goodgo.io
- http:
- paths:
- - path: /
- pathType: Prefix
- backend:
- service:
- name: chatservice
- port:
- number: 80
-```
-
## Health Checks
```csharp
@@ -452,5 +383,5 @@ builder.Services.AddHealthChecks()
- [ASP.NET Core SignalR](https://docs.microsoft.com/en-us/aspnet/core/signalr/)
- [SignalR Scale-out with Redis](https://docs.microsoft.com/en-us/aspnet/core/signalr/redis-backplane)
-- [Stateful Reconnect](https://learn.microsoft.com/en-us/aspnet/core/signalr/configuration)
+- [X3DH Protocol Specification](https://signal.org/docs/specifications/x3dh/)
- [Azure SignalR Service](https://docs.microsoft.com/en-us/azure/azure-signalr/)
diff --git a/services/chat-service-net/docs/en/README.md b/services/chat-service-net/docs/en/README.md
index 4e7a0cf7..8c1b3944 100644
--- a/services/chat-service-net/docs/en/README.md
+++ b/services/chat-service-net/docs/en/README.md
@@ -1,14 +1,15 @@
# Chat Service
-> Real-time chat service for GoodGo platform, built on ASP.NET Core SignalR.
+> Real-time chat service with End-to-End Encryption (E2EE) for GoodGo platform, built on ASP.NET Core SignalR.
## Overview
-Chat Service provides real-time communication capabilities in the microservices system with:
+Chat Service provides secure real-time communication in the microservices system with:
+- **End-to-End Encryption** - X3DH key exchange protocol with AES-256-GCM
- **Real-time Communication** - ASP.NET Core SignalR with WebSockets/SSE/Long Polling
- **Scalability** - Redis Backplane or Azure SignalR Service
-- **User & Group Management** - Chat rooms, cross-device user mapping
+- **User & Group Management** - Conversations, multi-device user mapping
- **AI Integration** - Smart chatbot with streaming response
- **High Performance** - MessagePack protocol
- **Resiliency** - Auto reconnect, Stateful Reconnect
@@ -54,7 +55,27 @@ dotnet run --project src/ChatService.API
## Feature Details
-### A. Real-time Communication
+### A. End-to-End Encryption (E2EE)
+
+| Feature | Description |
+|---------|-------------|
+| **X3DH Protocol** | Extended Triple Diffie-Hellman for key exchange |
+| **Identity Keys** | Long-term Curve25519 identity key pair |
+| **Signed Pre-Keys** | Rotated periodically (every 30 days) |
+| **One-Time Pre-Keys** | Forward secrecy for initial messages |
+| **AES-256-GCM** | Symmetric encryption for messages |
+
+```csharp
+// Server stores ONLY encrypted content - cannot decrypt without client's private key
+public class Message
+{
+ public string EncryptedContent { get; } // Base64 encoded
+ public string Nonce { get; } // IV for AES-GCM
+ public string? AuthTag { get; } // Authentication tag
+}
+```
+
+### B. Real-time Communication
| Feature | Description |
|---------|-------------|
@@ -62,19 +83,7 @@ dotnet run --project src/ChatService.API
| **Multi-Transport** | Auto-select WebSockets → SSE → Long Polling |
| **Streaming** | IAsyncEnumerable support for streaming AI responses |
-```csharp
-// Send message to group
-await Clients.Group(roomId).SendAsync("ReceiveMessage", message);
-
-// Streaming AI response
-public async IAsyncEnumerable StreamAIResponse(string prompt)
-{
- await foreach (var chunk in _aiService.StreamAsync(prompt))
- yield return chunk;
-}
-```
-
-### B. Scalability
+### C. Scalability
| Solution | Use Case |
|----------|----------|
@@ -82,91 +91,58 @@ public async IAsyncEnumerable StreamAIResponse(string prompt)
| **Azure SignalR Service** | Azure cloud, serverless scenarios |
| **Sticky Sessions** | Fallback when not using Azure SignalR |
-```csharp
-// Configure Redis Backplane
-builder.Services.AddSignalR()
- .AddStackExchangeRedis(connectionString, options => {
- options.Configuration.ChannelPrefix = RedisChannel.Literal("ChatService");
- });
-```
-
-### C. User & Group Management
-
-| Feature | Description |
-|---------|-------------|
-| **Groups** | Group connections by chat room |
-| **User Mapping** | Map ConnectionId → UserId via Claims |
-| **Persistent State** | Save group state to database |
-
-```csharp
-// Custom User ID Provider
-public class CustomUserIdProvider : IUserIdProvider
-{
- public string? GetUserId(HubConnectionContext connection)
- => connection.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
-}
-```
-
### D. AI Integration
| Feature | Description |
|---------|-------------|
-| **AI Assistant** | In-group chatbot (trigger: @gpt) |
+| **AI Assistant** | Chatbot in group (trigger: @gpt) |
| **Streaming Response** | Push response chunks in real-time |
| **Context History** | Save chat history for AI context |
-### E. MessagePack Protocol
-
-```csharp
-// Server configuration
-builder.Services.AddSignalR()
- .AddMessagePackProtocol();
-
-// Client configuration (JavaScript)
-const connection = new signalR.HubConnectionBuilder()
- .withUrl("/chatHub")
- .withHubProtocol(new signalR.protocols.msgpack.MessagePackHubProtocol())
- .build();
-```
-
-### F. Resiliency
-
-| Feature | Description |
-|---------|-------------|
-| **Auto Reconnect** | Client auto-reconnects |
-| **Stateful Reconnect** | Buffer data during interruption (.NET 8+) |
-
-```csharp
-// Server: Enable Stateful Reconnect
-builder.Services.AddSignalR(options => {
- options.EnableDetailedErrors = true;
- options.StatefulReconnectBufferSize = 32 * 1024; // 32KB buffer
-});
-
-// Client: Configure reconnect
-connection.WithAutomaticReconnect([0, 2000, 10000, 30000]);
-```
-
## API Endpoints
-### HTTP REST APIs
+### Conversations API
| Method | Endpoint | Description |
|--------|----------|-------------|
-| `GET` | `/api/v1/rooms` | Get chat room list |
-| `POST` | `/api/v1/rooms` | Create new chat room |
-| `GET` | `/api/v1/rooms/{id}/messages` | Get message history |
-| `POST` | `/api/v1/rooms/{id}/participants` | Add participant |
+| `POST` | `/api/conversations` | Create new conversation |
+| `GET` | `/api/conversations` | Get user's conversations |
+| `GET` | `/api/conversations/{id}` | Get specific conversation |
+
+### Messages API
+
+| Method | Endpoint | Description |
+|--------|----------|-------------|
+| `POST` | `/api/messages` | Send encrypted message |
+| `GET` | `/api/messages/conversation/{id}` | Get messages in conversation |
+| `POST` | `/api/messages/read` | Mark messages as read |
+
+### Keys API (E2EE)
+
+| Method | Endpoint | Description |
+|--------|----------|-------------|
+| `POST` | `/api/keys/register` | Register E2EE key bundle |
+| `POST` | `/api/keys/rotate` | Rotate signed pre-key |
+| `POST` | `/api/keys/prekeys` | Upload one-time pre-keys |
+| `GET` | `/api/keys/bundle/{userId}` | Get user's key bundle |
+| `GET` | `/api/keys/my-bundle` | Get own key bundle status |
### SignalR Hub Methods
| Method | Direction | Description |
|--------|-----------|-------------|
-| `SendMessage` | Client → Server | Send message |
-| `JoinRoom` | Client → Server | Join room |
-| `LeaveRoom` | Client → Server | Leave room |
-| `ReceiveMessage` | Server → Client | Receive message |
-| `UserJoined` | Server → Client | New user notification |
+| `JoinRoom` | Client → Server | Join conversation room |
+| `LeaveRoom` | Client → Server | Leave conversation room |
+| `SendMessage` | Client → Server | Send encrypted message |
+| `SendTypingIndicator` | Client → Server | Send typing status |
+| `MarkMessageRead` | Client → Server | Mark message as read |
+| `StreamAIResponse` | Client → Server | Request AI streaming response |
+| `ReceiveMessage` | Server → Client | Receive new message |
+| `UserJoined` | Server → Client | User joined notification |
+| `UserLeft` | Server → Client | User left notification |
+| `TypingIndicator` | Server → Client | Typing indicator |
+| `MessageDelivered` | Server → Client | Message delivered status |
+| `MessageRead` | Server → Client | Message read status |
### Health Endpoints
@@ -226,7 +202,7 @@ See [ARCHITECTURE.md](./ARCHITECTURE.md) for detailed Kubernetes configuration w
- [ASP.NET Core SignalR](https://docs.microsoft.com/en-us/aspnet/core/signalr/)
- [Redis Backplane](https://docs.microsoft.com/en-us/aspnet/core/signalr/redis-backplane)
- [Azure SignalR Service](https://docs.microsoft.com/en-us/azure/azure-signalr/)
-- [MessagePack Protocol](https://docs.microsoft.com/en-us/aspnet/core/signalr/messagepackhubprotocol)
+- [X3DH Protocol](https://signal.org/docs/specifications/x3dh/)
## License
diff --git a/services/chat-service-net/docs/vi/ARCHITECTURE.md b/services/chat-service-net/docs/vi/ARCHITECTURE.md
index 85a15c8d..f748018c 100644
--- a/services/chat-service-net/docs/vi/ARCHITECTURE.md
+++ b/services/chat-service-net/docs/vi/ARCHITECTURE.md
@@ -1,6 +1,6 @@
# Tài Liệu Kiến Trúc Chat Service
-> Kiến trúc chi tiết cho Chat Service với SignalR, scalability patterns, và AI integration.
+> Kiến trúc chi tiết cho Chat Service với E2EE, SignalR, scalability patterns, và AI integration.
## Tổng Quan Kiến Trúc
@@ -43,12 +43,138 @@ graph TB
CS1 & CS2 & CS3 --> RC
CS1 & CS2 & CS3 --> AI
- style LB fill:#3498db,stroke:#2980b9,color:#fff
- style RD fill:#e74c3c,stroke:#c0392b,color:#fff
- style PG fill:#27ae60,stroke:#1e8449,color:#fff
- style AI fill:#9b59b6,stroke:#7d3c98,color:#fff
+ style LB fill:#3498DB,color:#ECF0F1,stroke:#2980B9,stroke-width:3px
+ style RD fill:#C0392B,color:#ECF0F1,stroke:#A93226,stroke-width:2px
+ style PG fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px
+ style AI fill:#8E44AD,color:#ECF0F1,stroke:#7D3C98,stroke-width:2px
```
+## Mã Hóa Đầu-cuối (E2EE)
+
+### Giao Thức Trao Đổi Khóa X3DH
+
+Service triển khai giao thức Extended Triple Diffie-Hellman (X3DH) để thiết lập phiên mã hóa:
+
+```mermaid
+sequenceDiagram
+ participant Alice as Alice (Người gửi)
+ participant Server as Chat Server
+ participant Bob as Bob (Người nhận)
+
+ Note over Bob,Server: Bob đăng ký keys
+ Bob->>Server: Đăng ký Key Bundle
(Identity, SignedPreKey, OneTimePreKeys)
+ Server->>Server: Lưu chỉ public keys
+
+ Note over Alice,Server: Alice khởi tạo session
+ Alice->>Server: Yêu cầu Key Bundle của Bob
+ Server->>Alice: {IdentityKey, SignedPreKey, OneTimePreKey}
+ Alice->>Alice: X3DH → Session Key
+ Alice->>Alice: AES-256-GCM mã hóa tin nhắn
+ Alice->>Server: Gửi tin nhắn đã mã hóa
+ Server->>Bob: Chuyển tin nhắn đã mã hóa
+ Bob->>Bob: X3DH → Session Key
+ Bob->>Bob: AES-256-GCM giải mã tin nhắn
+
+ style Server fill:#2C3E50,color:#ECF0F1,stroke:#34495E,stroke-width:3px
+```
+
+### Các Thành Phần Khóa
+
+| Thành phần | Mô tả | Lưu trữ |
+|------------|-------|---------|
+| **Identity Key** | Cặp khóa Curve25519 dài hạn | Private: Chỉ client |
+| **Signed Pre-Key** | Xoay vòng mỗi 30 ngày | Public: Server |
+| **One-Time Pre-Keys** | Tiêu thụ mỗi session | Public: Server (xóa sau khi dùng) |
+| **Session Key** | Derive qua X3DH | Không lưu trữ |
+
+### Tính Năng Bảo Mật
+
+- ✅ **Forward Secrecy**: Khóa bị lộ không tiết lộ tin nhắn cũ
+- ✅ **Zero-Knowledge Server**: Server không thể giải mã tin nhắn
+- ✅ **Deniability**: Không có bằng chứng mật mã về tác giả
+
+## Domain Model
+
+### Aggregate Roots
+
+```mermaid
+erDiagram
+ Conversation ||--o{ Message : contains
+ Conversation ||--o{ ConversationParticipant : has
+ ChatUser ||--o{ Conversation : participates
+ ChatUser ||--|| UserKeyBundle : has
+ ChatUser ||--o{ OneTimePreKey : owns
+
+ Conversation {
+ uuid Id PK
+ string Name
+ string AvatarUrl
+ enum Type "Direct|Group"
+ uuid CreatorId
+ datetime CreatedAt
+ datetime LastActivityAt
+ }
+
+ Message {
+ uuid Id PK
+ uuid ConversationId FK
+ uuid SenderId FK
+ string EncryptedContent
+ string Nonce
+ string AuthTag
+ enum Type "Text|Image|File|System"
+ enum Status "Sent|Delivered|Read|Failed"
+ datetime CreatedAt
+ }
+
+ ConversationParticipant {
+ uuid Id PK
+ uuid ConversationId FK
+ uuid ChatUserId FK
+ enum Role "Owner|Admin|Member"
+ datetime JoinedAt
+ datetime LastReadAt
+ }
+
+ ChatUser {
+ uuid Id PK
+ string IdentityUserId
+ string DisplayName
+ string AvatarUrl
+ enum Status "Online|Away|Offline"
+ datetime LastSeenAt
+ }
+
+ UserKeyBundle {
+ string IdentityPublicKey
+ string SignedPreKey
+ string SignedPreKeySignature
+ datetime SignedPreKeyTimestamp
+ }
+
+ OneTimePreKey {
+ uuid Id PK
+ int KeyId
+ string PublicKey
+ bool IsUsed
+ datetime CreatedAt
+ }
+```
+
+### Domain Events
+
+| Event | Trigger | Handler |
+|-------|---------|---------|
+| `ConversationCreatedDomainEvent` | Hội thoại mới được tạo | Thông báo participants |
+| `MessageSentDomainEvent` | Tin nhắn được gửi | Cập nhật last_activity, broadcast |
+| `MessageDeliveredDomainEvent` | Tin nhắn đã gửi đến | Thông báo sender |
+| `MessageReadDomainEvent` | Tin nhắn đã đọc | Thông báo sender |
+| `UserJoinedRoomDomainEvent` | User tham gia room | Thông báo members |
+| `UserLeftRoomDomainEvent` | User rời room | Thông báo members |
+| `TypingDomainEvent` | User đang gõ | Broadcast to room |
+| `ChatUserCreatedDomainEvent` | Chat user mới | Khởi tạo presence |
+| `UserKeyBundleUpdatedDomainEvent` | Keys được cập nhật | Invalidate cached keys |
+
## SignalR Hub Architecture
### Connection Lifecycle
@@ -63,74 +189,47 @@ sequenceDiagram
Client->>Hub: Connect (JWT Token)
Hub->>Hub: OnConnectedAsync()
- Hub->>DB: Load user rooms
- Hub->>Groups: AddToGroupAsync(roomId)
+ Hub->>DB: Load danh sách hội thoại
+ Hub->>Groups: AddToGroupAsync(conversationId)
Hub->>Redis: Publish(UserOnline)
Hub-->>Client: Connected
- Note over Client,Hub: User is now online
+ Note over Client,Hub: User hiện đang online
- Client->>Hub: SendMessage(roomId, content)
- Hub->>DB: Save message
+ Client->>Hub: SendMessage(conversationId, encryptedContent)
+ Hub->>DB: Lưu tin nhắn đã mã hóa
Hub->>Redis: Publish(NewMessage)
- Redis-->>Hub: Broadcast to all instances
- Hub->>Groups: Clients.Group(roomId)
+ Redis-->>Hub: Broadcast đến tất cả instances
+ Hub->>Groups: Clients.Group(conversationId)
Hub-->>Client: ReceiveMessage
+
+ style Hub fill:#2C3E50,color:#ECF0F1,stroke:#34495E,stroke-width:3px
+ style Redis fill:#C0392B,color:#ECF0F1,stroke:#A93226,stroke-width:2px
+ style DB fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px
```
-### Hub Implementation
+### Hub Methods
```csharp
-public class ChatHub : Hub
+public class ChatHub : Hub
{
- private readonly IChatRoomRepository _roomRepository;
- private readonly IMessageRepository _messageRepository;
- private readonly IAIService _aiService;
+ // Quản lý kết nối
+ public override Task OnConnectedAsync();
+ public override Task OnDisconnectedAsync(Exception? exception);
- public async Task JoinRoom(Guid roomId)
- {
- var userId = Context.UserIdentifier;
-
- // Thêm vào SignalR Group
- await Groups.AddToGroupAsync(Context.ConnectionId, roomId.ToString());
-
- // Thông báo cho members khác
- await Clients.Group(roomId.ToString())
- .UserJoined(userId, roomId);
-
- // Lưu trạng thái vào DB
- await _roomRepository.AddParticipantAsync(roomId, userId);
- }
+ // Quản lý phòng
+ public Task JoinRoom(Guid conversationId);
+ public Task LeaveRoom(Guid conversationId);
- public async Task SendMessage(Guid roomId, string content)
- {
- var message = new Message(roomId, Context.UserIdentifier, content);
- await _messageRepository.AddAsync(message);
-
- // Broadcast tin nhắn
- await Clients.Group(roomId.ToString())
- .ReceiveMessage(message.ToDto());
-
- // Kiểm tra AI trigger
- if (content.StartsWith("@gpt "))
- {
- await StreamAIResponse(roomId, content[5..]);
- }
- }
+ // Nhắn tin
+ public Task SendMessage(Guid conversationId, string encryptedContent,
+ string nonce, string? authTag);
+ public Task SendTypingIndicator(Guid conversationId, bool isTyping);
+ public Task MarkMessageRead(Guid conversationId, Guid messageId);
- public async IAsyncEnumerable StreamAIResponse(
- Guid roomId,
- string prompt,
- [EnumeratorCancellation] CancellationToken ct = default)
- {
- // Lấy lịch sử chat cho context
- var history = await _messageRepository.GetHistoryAsync(roomId, 20);
-
- await foreach (var chunk in _aiService.StreamAsync(prompt, history, ct))
- {
- yield return chunk;
- }
- }
+ // Tích hợp AI
+ public IAsyncEnumerable StreamAIResponse(Guid conversationId,
+ string prompt);
}
```
@@ -156,7 +255,9 @@ graph LR
CH -->|Subscribe| H2
H2 -->|Deliver| C2
- style CH fill:#e74c3c,stroke:#c0392b,color:#fff
+ style CH fill:#C0392B,color:#ECF0F1,stroke:#A93226,stroke-width:2px
+ style H1 fill:#2C3E50,color:#ECF0F1,stroke:#34495E,stroke-width:2px
+ style H2 fill:#2C3E50,color:#ECF0F1,stroke:#34495E,stroke-width:2px
```
### Cấu Hình Scaling
@@ -167,86 +268,6 @@ graph LR
| **Azure SignalR** | Serverless, không sticky sessions | Vendor lock-in, chi phí |
| **Sticky Sessions** | Đơn giản nhất | Không hoàn hảo cho failover |
-```csharp
-// Redis Backplane Configuration
-builder.Services.AddSignalR()
- .AddStackExchangeRedis(options =>
- {
- options.Configuration.ChannelPrefix =
- RedisChannel.Literal("ChatService");
- options.Configuration.AbortOnConnectFail = false;
- });
-
-// Azure SignalR Service
-builder.Services.AddSignalR()
- .AddAzureSignalR(options =>
- {
- options.ConnectionString = azureSignalRConnectionString;
- options.ServerStickyMode = ServerStickyMode.Required;
- });
-```
-
-## User & Group Management
-
-### User Mapping
-
-```mermaid
-graph TB
- subgraph "User Mapping"
- U1[User ID: user-123]
- C1[Connection 1
Phone]
- C2[Connection 2
Laptop]
- C3[Connection 3
Tablet]
- end
-
- U1 --> C1 & C2 & C3
-
- style U1 fill:#3498db,stroke:#2980b9,color:#fff
-```
-
-### IUserIdProvider
-
-```csharp
-public class ClaimsUserIdProvider : IUserIdProvider
-{
- public string? GetUserId(HubConnectionContext connection)
- {
- // Lấy User ID từ JWT Claims
- return connection.User?
- .FindFirst(ClaimTypes.NameIdentifier)?.Value;
- }
-}
-
-// Registration
-builder.Services.AddSingleton();
-```
-
-### Group State Management
-
-```csharp
-// Vấn đề: SignalR Groups không persist khi restart
-// Giải pháp: Lưu trữ trong database
-
-public class GroupStateService
-{
- private readonly IDistributedCache _cache;
- private readonly IChatRoomRepository _repository;
-
- public async Task RestoreUserGroups(string userId, string connectionId)
- {
- var rooms = await _repository.GetUserRoomsAsync(userId);
-
- foreach (var room in rooms)
- {
- await _hubContext.Groups.AddToGroupAsync(
- connectionId,
- room.Id.ToString()
- );
- }
- }
-}
-```
-
## AI Integration Flow
```mermaid
@@ -258,7 +279,7 @@ sequenceDiagram
participant DB as Database
User->>Hub: "@gpt Giải thích DDD"
- Hub->>DB: Load history (20 messages)
+ Hub->>DB: Load lịch sử (20 tin nhắn)
Hub->>AI: StreamAsync(prompt, history)
AI->>OpenAI: ChatCompletion (stream: true)
@@ -268,76 +289,12 @@ sequenceDiagram
Hub-->>User: ReceiveAIChunk
end
- Hub->>DB: Save AI response
+ Hub->>DB: Lưu AI response
Hub-->>User: AIResponseComplete
-```
-
-## Domain Model
-
-### Aggregate Roots
-
-```mermaid
-erDiagram
- ChatRoom ||--o{ Message : contains
- ChatRoom ||--o{ Participant : has
- ChatRoom {
- uuid id PK
- string name
- enum type
- uuid owner_id
- timestamp created_at
- }
- Message {
- uuid id PK
- uuid room_id FK
- uuid sender_id
- string content
- enum type
- timestamp sent_at
- }
-
- Participant {
- uuid id PK
- uuid room_id FK
- uuid user_id
- enum role
- timestamp joined_at
- timestamp last_read
- }
-```
-
-### Domain Events
-
-| Event | Trigger | Handler |
-|-------|---------|---------|
-| `MessageSentEvent` | Tin nhắn mới | Update last_activity, broadcast |
-| `UserJoinedRoomEvent` | User join room | Notify members |
-| `UserLeftRoomEvent` | User leave room | Notify members |
-| `RoomCreatedEvent` | Tạo room mới | Add creator as admin |
-
-## MessagePack Protocol
-
-### So sánh JSON vs MessagePack
-
-| Metric | JSON | MessagePack |
-|--------|------|-------------|
-| Size | 100% | ~50-70% |
-| Parse time | 1x | 0.5-0.8x |
-| Type safety | Weak | Strong |
-
-### Cấu Hình
-
-```csharp
-// Server
-builder.Services.AddSignalR()
- .AddMessagePackProtocol(options =>
- {
- options.SerializerOptions =
- MessagePackSerializerOptions.Standard
- .WithSecurity(MessagePackSecurity.UntrustedData)
- .WithCompression(MessagePackCompression.Lz4BlockArray);
- });
+ style Hub fill:#2C3E50,color:#ECF0F1,stroke:#34495E,stroke-width:3px
+ style AI fill:#8E44AD,color:#ECF0F1,stroke:#7D3C98,stroke-width:2px
+ style OpenAI fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px
```
## Resiliency Patterns
@@ -347,11 +304,11 @@ builder.Services.AddSignalR()
```mermaid
stateDiagram-v2
[*] --> Connected
- Connected --> Disconnected: Connection lost
- Disconnected --> Reconnecting: Auto retry
- Reconnecting --> Connected: Success
- Reconnecting --> Reconnecting: Retry with backoff
- Reconnecting --> Disconnected: Max retries exceeded
+ Connected --> Disconnected: Mất kết nối
+ Disconnected --> Reconnecting: Tự động retry
+ Reconnecting --> Connected: Thành công
+ Reconnecting --> Reconnecting: Retry với backoff
+ Reconnecting --> Disconnected: Vượt quá max retries
Disconnected --> [*]: User logout
```
@@ -413,32 +370,6 @@ spec:
targetPort: 8080
```
-### Ingress với WebSocket Support
-
-```yaml
-apiVersion: networking.k8s.io/v1
-kind: Ingress
-metadata:
- name: chatservice
- annotations:
- nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
- nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
- nginx.ingress.kubernetes.io/upstream-hash-by: "$request_uri"
- nginx.ingress.kubernetes.io/affinity: "cookie"
-spec:
- rules:
- - host: chat.goodgo.io
- http:
- paths:
- - path: /
- pathType: Prefix
- backend:
- service:
- name: chatservice
- port:
- number: 80
-```
-
## Health Checks
```csharp
@@ -452,5 +383,5 @@ builder.Services.AddHealthChecks()
- [ASP.NET Core SignalR](https://docs.microsoft.com/en-us/aspnet/core/signalr/)
- [SignalR Scale-out with Redis](https://docs.microsoft.com/en-us/aspnet/core/signalr/redis-backplane)
-- [Stateful Reconnect](https://learn.microsoft.com/en-us/aspnet/core/signalr/configuration)
+- [X3DH Protocol Specification](https://signal.org/docs/specifications/x3dh/)
- [Azure SignalR Service](https://docs.microsoft.com/en-us/azure/azure-signalr/)
diff --git a/services/chat-service-net/docs/vi/README.md b/services/chat-service-net/docs/vi/README.md
index 2bd66cc3..422011c3 100644
--- a/services/chat-service-net/docs/vi/README.md
+++ b/services/chat-service-net/docs/vi/README.md
@@ -1,14 +1,15 @@
# Chat Service
-> Dịch vụ Chat thời gian thực cho nền tảng GoodGo, xây dựng trên ASP.NET Core SignalR.
+> Dịch vụ Chat thời gian thực với Mã hóa Đầu-cuối (E2EE) cho nền tảng GoodGo, xây dựng trên ASP.NET Core SignalR.
## Tổng Quan
-Chat Service cung cấp khả năng giao tiếp thời gian thực trong hệ thống microservices với:
+Chat Service cung cấp khả năng giao tiếp bảo mật thời gian thực trong hệ thống microservices với:
+- **Mã hóa Đầu-cuối** - Giao thức trao đổi khóa X3DH với AES-256-GCM
- **Giao tiếp thời gian thực** - ASP.NET Core SignalR với WebSockets/SSE/Long Polling
- **Khả năng mở rộng** - Redis Backplane hoặc Azure SignalR Service
-- **Quản lý người dùng & nhóm** - Phòng chat, ánh xạ user across devices
+- **Quản lý người dùng & nhóm** - Hội thoại, ánh xạ user đa thiết bị
- **Tích hợp AI** - Chatbot thông minh với streaming response
- **Hiệu năng cao** - Giao thức MessagePack
- **Khả năng phục hồi** - Auto reconnect, Stateful Reconnect
@@ -54,59 +55,42 @@ dotnet run --project src/ChatService.API
## Tính Năng Chi Tiết
-### A. Giao Tiếp Thời Gian Thực
+### A. Mã Hóa Đầu-cuối (E2EE)
| Tính năng | Mô tả |
|-----------|-------|
-| **SignalR Hub** | Hub trung tâm xử lý tất cả real-time connections |
+| **Giao thức X3DH** | Extended Triple Diffie-Hellman cho trao đổi khóa |
+| **Identity Keys** | Cặp khóa định danh Curve25519 dài hạn |
+| **Signed Pre-Keys** | Xoay vòng định kỳ (mỗi 30 ngày) |
+| **One-Time Pre-Keys** | Forward secrecy cho tin nhắn đầu tiên |
+| **AES-256-GCM** | Mã hóa đối xứng cho tin nhắn |
+
+```csharp
+// Server CHỈ lưu nội dung đã mã hóa - không thể giải mã mà không có private key của client
+public class Message
+{
+ public string EncryptedContent { get; } // Mã hóa Base64
+ public string Nonce { get; } // IV cho AES-GCM
+ public string? AuthTag { get; } // Authentication tag
+}
+```
+
+### B. Giao Tiếp Thời Gian Thực
+
+| Tính năng | Mô tả |
+|-----------|-------|
+| **SignalR Hub** | Hub trung tâm xử lý tất cả kết nối realtime |
| **Multi-Transport** | Tự động chọn WebSockets → SSE → Long Polling |
| **Streaming** | Hỗ trợ IAsyncEnumerable cho streaming AI responses |
-```csharp
-// Gửi tin nhắn đến nhóm
-await Clients.Group(roomId).SendAsync("ReceiveMessage", message);
-
-// Streaming AI response
-public async IAsyncEnumerable StreamAIResponse(string prompt)
-{
- await foreach (var chunk in _aiService.StreamAsync(prompt))
- yield return chunk;
-}
-```
-
-### B. Khả Năng Mở Rộng
+### C. Khả Năng Mở Rộng
| Giải pháp | Use Case |
|-----------|----------|
-| **Redis Backplane** | On-premise, multi-instance deployment |
+| **Redis Backplane** | On-premise, triển khai multi-instance |
| **Azure SignalR Service** | Azure cloud, serverless scenarios |
| **Sticky Sessions** | Fallback khi không dùng Azure SignalR |
-```csharp
-// Cấu hình Redis Backplane
-builder.Services.AddSignalR()
- .AddStackExchangeRedis(connectionString, options => {
- options.Configuration.ChannelPrefix = RedisChannel.Literal("ChatService");
- });
-```
-
-### C. Quản Lý Người Dùng & Nhóm
-
-| Tính năng | Mô tả |
-|-----------|-------|
-| **Groups** | Gom kết nối theo phòng chat |
-| **User Mapping** | Ánh xạ ConnectionId → UserId qua Claims |
-| **Persistent State** | Lưu trạng thái nhóm vào database |
-
-```csharp
-// Custom User ID Provider
-public class CustomUserIdProvider : IUserIdProvider
-{
- public string? GetUserId(HubConnectionContext connection)
- => connection.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
-}
-```
-
### D. Tích Hợp AI
| Tính năng | Mô tả |
@@ -115,58 +99,50 @@ public class CustomUserIdProvider : IUserIdProvider
| **Streaming Response** | Đẩy từng phần câu trả lời realtime |
| **Context History** | Lưu lịch sử chat cho context AI |
-### E. Giao Thức MessagePack
-
-```csharp
-// Server configuration
-builder.Services.AddSignalR()
- .AddMessagePackProtocol();
-
-// Client configuration (JavaScript)
-const connection = new signalR.HubConnectionBuilder()
- .withUrl("/chatHub")
- .withHubProtocol(new signalR.protocols.msgpack.MessagePackHubProtocol())
- .build();
-```
-
-### F. Khả Năng Phục Hồi
-
-| Tính năng | Mô tả |
-|-----------|-------|
-| **Auto Reconnect** | Client tự động kết nối lại |
-| **Stateful Reconnect** | Buffer dữ liệu khi gián đoạn (.NET 8+) |
-
-```csharp
-// Server: Bật Stateful Reconnect
-builder.Services.AddSignalR(options => {
- options.EnableDetailedErrors = true;
- options.StatefulReconnectBufferSize = 32 * 1024; // 32KB buffer
-});
-
-// Client: Cấu hình reconnect
-connection.WithAutomaticReconnect([0, 2000, 10000, 30000]);
-```
-
## API Endpoints
-### HTTP REST APIs
+### Conversations API
| Method | Endpoint | Mô tả |
|--------|----------|-------|
-| `GET` | `/api/v1/rooms` | Lấy danh sách phòng chat |
-| `POST` | `/api/v1/rooms` | Tạo phòng chat mới |
-| `GET` | `/api/v1/rooms/{id}/messages` | Lấy lịch sử tin nhắn |
-| `POST` | `/api/v1/rooms/{id}/participants` | Thêm thành viên |
+| `POST` | `/api/conversations` | Tạo hội thoại mới |
+| `GET` | `/api/conversations` | Lấy danh sách hội thoại của user |
+| `GET` | `/api/conversations/{id}` | Lấy hội thoại cụ thể |
+
+### Messages API
+
+| Method | Endpoint | Mô tả |
+|--------|----------|-------|
+| `POST` | `/api/messages` | Gửi tin nhắn đã mã hóa |
+| `GET` | `/api/messages/conversation/{id}` | Lấy tin nhắn trong hội thoại |
+| `POST` | `/api/messages/read` | Đánh dấu tin nhắn đã đọc |
+
+### Keys API (E2EE)
+
+| Method | Endpoint | Mô tả |
+|--------|----------|-------|
+| `POST` | `/api/keys/register` | Đăng ký E2EE key bundle |
+| `POST` | `/api/keys/rotate` | Xoay vòng signed pre-key |
+| `POST` | `/api/keys/prekeys` | Upload one-time pre-keys |
+| `GET` | `/api/keys/bundle/{userId}` | Lấy key bundle của user |
+| `GET` | `/api/keys/my-bundle` | Lấy trạng thái key bundle của mình |
### SignalR Hub Methods
-| Method | Direction | Mô tả |
-|--------|-----------|-------|
-| `SendMessage` | Client → Server | Gửi tin nhắn |
-| `JoinRoom` | Client → Server | Tham gia phòng |
-| `LeaveRoom` | Client → Server | Rời phòng |
-| `ReceiveMessage` | Server → Client | Nhận tin nhắn |
-| `UserJoined` | Server → Client | Thông báo user mới |
+| Method | Hướng | Mô tả |
+|--------|-------|-------|
+| `JoinRoom` | Client → Server | Tham gia phòng hội thoại |
+| `LeaveRoom` | Client → Server | Rời phòng hội thoại |
+| `SendMessage` | Client → Server | Gửi tin nhắn đã mã hóa |
+| `SendTypingIndicator` | Client → Server | Gửi trạng thái đang gõ |
+| `MarkMessageRead` | Client → Server | Đánh dấu tin nhắn đã đọc |
+| `StreamAIResponse` | Client → Server | Yêu cầu AI streaming response |
+| `ReceiveMessage` | Server → Client | Nhận tin nhắn mới |
+| `UserJoined` | Server → Client | Thông báo user tham gia |
+| `UserLeft` | Server → Client | Thông báo user rời đi |
+| `TypingIndicator` | Server → Client | Chỉ báo đang gõ |
+| `MessageDelivered` | Server → Client | Trạng thái tin nhắn đã gửi |
+| `MessageRead` | Server → Client | Trạng thái tin nhắn đã đọc |
### Health Endpoints
@@ -226,7 +202,7 @@ Xem [ARCHITECTURE.md](./ARCHITECTURE.md) để biết chi tiết cấu hình Kub
- [ASP.NET Core SignalR](https://docs.microsoft.com/en-us/aspnet/core/signalr/)
- [Redis Backplane](https://docs.microsoft.com/en-us/aspnet/core/signalr/redis-backplane)
- [Azure SignalR Service](https://docs.microsoft.com/en-us/azure/azure-signalr/)
-- [MessagePack Protocol](https://docs.microsoft.com/en-us/aspnet/core/signalr/messagepackhubprotocol)
+- [Giao thức X3DH](https://signal.org/docs/specifications/x3dh/)
## Giấy Phép
diff --git a/services/membership-service-net/README.md b/services/membership-service-net/README.md
new file mode 100644
index 00000000..4e88e2ba
--- /dev/null
+++ b/services/membership-service-net/README.md
@@ -0,0 +1,34 @@
+# Membership Service
+
+> **EN**: [English Documentation](docs/en/README.md)
+> **VI**: [Tài liệu Tiếng Việt](docs/vi/README.md)
+
+## Quick Links
+
+- 📖 [Architecture Documentation](docs/en/ARCHITECTURE.md) / [Tài liệu Kiến trúc](docs/vi/ARCHITECTURE.md)
+- 🚀 [Quick Start](docs/en/README.md#quick-start)
+- 🔧 [Configuration](docs/en/README.md#configuration)
+
+## Tech Stack
+
+- .NET 10
+- PostgreSQL
+- Redis (caching)
+- MediatR (CQRS)
+- FluentValidation
+
+## Development
+
+```bash
+# Restore and build
+dotnet restore
+dotnet build
+
+# Run the API
+dotnet run --project src/MembershipService.API
+
+# Run tests
+dotnet test
+```
+
+See detailed documentation in [docs/en/](docs/en/) or [docs/vi/](docs/vi/).
diff --git a/services/membership-service-net/docs/en/ARCHITECTURE.md b/services/membership-service-net/docs/en/ARCHITECTURE.md
index 7109ac97..04dca07b 100644
--- a/services/membership-service-net/docs/en/ARCHITECTURE.md
+++ b/services/membership-service-net/docs/en/ARCHITECTURE.md
@@ -584,7 +584,6 @@ PUT /api/v1/members/{id} # Update profile
```
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)
diff --git a/services/membership-service-net/docs/en/README.md b/services/membership-service-net/docs/en/README.md
index b2c9fa7c..8577c15d 100644
--- a/services/membership-service-net/docs/en/README.md
+++ b/services/membership-service-net/docs/en/README.md
@@ -121,9 +121,11 @@ membership-service-net/
| `GET` | `/api/v1/members` | Get paginated members (with search) | Yes |
| `GET` | `/api/v1/members/{id}` | Get member by ID | Yes |
| `GET` | `/api/v1/members/me` | Get current user's profile | Yes |
+| `GET` | `/api/v1/members/{id}/progress` | Get member's level progress | Yes |
+| `GET` | `/api/v1/members/{id}/experience` | Get member's EXP history | Yes |
| `POST` | `/api/v1/members` | Create new member | Yes |
+| `POST` | `/api/v1/members/{id}/experience` | Add experience points | Yes |
| `PUT` | `/api/v1/members/{id}` | Update member profile | Yes |
-| `PUT` | `/api/v1/members/{id}/level` | Change membership level | Yes |
### Health Endpoints
diff --git a/services/membership-service-net/docs/vi/ARCHITECTURE.md b/services/membership-service-net/docs/vi/ARCHITECTURE.md
index d27d7e6c..bcdb7506 100644
--- a/services/membership-service-net/docs/vi/ARCHITECTURE.md
+++ b/services/membership-service-net/docs/vi/ARCHITECTURE.md
@@ -584,7 +584,6 @@ PUT /api/v1/members/{id} # Cập nhật hồ sơ
```
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)
diff --git a/services/membership-service-net/docs/vi/README.md b/services/membership-service-net/docs/vi/README.md
index 6fd7bfed..317ac9d4 100644
--- a/services/membership-service-net/docs/vi/README.md
+++ b/services/membership-service-net/docs/vi/README.md
@@ -121,9 +121,11 @@ membership-service-net/
| `GET` | `/api/v1/members` | Lấy danh sách thành viên (phân trang, search) | Có |
| `GET` | `/api/v1/members/{id}` | Lấy thành viên theo ID | Có |
| `GET` | `/api/v1/members/me` | Lấy hồ sơ người dùng hiện tại | Có |
+| `GET` | `/api/v1/members/{id}/progress` | Lấy tiến độ level của thành viên | Có |
+| `GET` | `/api/v1/members/{id}/experience` | Lấy lịch sử EXP của thành viên | Có |
| `POST` | `/api/v1/members` | Tạo thành viên mới | Có |
+| `POST` | `/api/v1/members/{id}/experience` | Thêm điểm kinh nghiệm | Có |
| `PUT` | `/api/v1/members/{id}` | Cập nhật hồ sơ thành viên | Có |
-| `PUT` | `/api/v1/members/{id}/level` | Thay đổi cấp thành viên | Có |
### Health Endpoints
diff --git a/services/wallet-service-net/docs/en/ARCHITECTURE.md b/services/wallet-service-net/docs/en/ARCHITECTURE.md
index 60532c62..ebd2b040 100644
--- a/services/wallet-service-net/docs/en/ARCHITECTURE.md
+++ b/services/wallet-service-net/docs/en/ARCHITECTURE.md
@@ -4,18 +4,36 @@
The Wallet Service manages digital wallets and loyalty point accounts for the GoodGo Platform.
-```
-┌─────────────────────────────────────────────────────────────┐
-│ Wallet Service │
-├─────────────────────────────────────────────────────────────┤
-│ API Layer (Controllers, CQRS) │
-├─────────────────────────────────────────────────────────────┤
-│ Domain Layer (Wallet, PointAccount Aggregates) │
-├─────────────────────────────────────────────────────────────┤
-│ Infrastructure Layer (EF Core, Repositories) │
-├─────────────────────────────────────────────────────────────┤
-│ PostgreSQL Database │
-└─────────────────────────────────────────────────────────────┘
+```mermaid
+flowchart TB
+ subgraph API["🌐 API Layer"]
+ Controllers["Controllers"]
+ Commands["Commands"]
+ Queries["Queries"]
+ end
+
+ subgraph Domain["💎 Domain Layer"]
+ Wallet["Wallet Aggregate"]
+ PointAccount["PointAccount Aggregate"]
+ end
+
+ subgraph Infra["⚙️ Infrastructure Layer"]
+ Repos["Repositories"]
+ EF["EF Core"]
+ end
+
+ subgraph DB["💾 PostgreSQL"]
+ Tables["Tables"]
+ end
+
+ API --> Domain
+ Domain --> Infra
+ Infra --> DB
+
+ style API fill:#3498DB,color:#ECF0F1,stroke:#2980B9,stroke-width:3px
+ style Domain fill:#8E44AD,color:#ECF0F1,stroke:#7D3C98,stroke-width:2px
+ style Infra fill:#E67E22,color:#ECF0F1,stroke:#D35400,stroke-width:2px
+ style DB fill:#34495E,color:#ECF0F1,stroke:#2C3E50,stroke-width:2px
```
## Architecture Patterns
@@ -23,22 +41,33 @@ The Wallet Service manages digital wallets and loyalty point accounts for the Go
### Domain-Driven Design (DDD)
- **Aggregates**: Wallet, PointAccount
-- **Entities**: WalletTransaction, PointTransaction
-- **Value Objects**: Money, PointBalance
-- **Domain Events**: WalletCreated, BalanceChanged, PointsEarned
+- **Entities**: WalletTransaction, PointTransaction, HoldItem, WalletItem
+- **Value Objects**: Money, CurrencyType
+- **Domain Events**: WalletCreated, BalanceChanged, PointsEarned, EscrowHeld, EscrowExecuted
### CQRS Pattern
-```
-Commands (Write) Queries (Read)
- │ │
- ▼ ▼
-CreateWallet GetWalletQuery
-DepositCommand GetTransactionsQuery
-WithdrawCommand GetPointAccountQuery
-TransferCommand GetBalanceSummaryQuery
-EarnPointsCommand
-SpendPointsCommand
+```mermaid
+flowchart LR
+ subgraph Commands["Commands (Write)"]
+ C1["CreateWallet"]
+ C2["Deposit/Withdraw"]
+ C3["Exchange"]
+ C4["Hold/Execute/Release"]
+ C5["EarnPoints/SpendPoints"]
+ C6["Admin Commands"]
+ end
+
+ subgraph Queries["Queries (Read)"]
+ Q1["GetWallet"]
+ Q2["GetTransactions"]
+ Q3["GetPointAccount"]
+ Q4["GetStatistics"]
+ Q5["Admin Queries"]
+ end
+
+ style Commands fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px
+ style Queries fill:#3498DB,color:#ECF0F1,stroke:#2980B9,stroke-width:2px
```
## Domain Model
@@ -50,32 +79,68 @@ classDiagram
class Wallet {
+Guid Id
+Guid UserId
- +Money Balance
+WalletStatus Status
- +List~WalletTransaction~ Transactions
- +Deposit(Money, description, ref)
- +Withdraw(Money, description, ref)
+ +CurrencyType DefaultCurrency
+ +List~WalletItem~ Items
+ +List~HoldItem~ Holds
+ +Deposit(amount, currency, desc, ref)
+ +Withdraw(amount, currency, desc, ref)
+ +Exchange(fromAmount, fromCurrency, toCurrency)
+ +Hold(amount, currency, refType, refId, desc)
+ +ExecuteHold(holdId, amount, ref)
+ +ReleaseHold(holdId, amount)
+Freeze()
+Unfreeze()
- +Close()
+ }
+
+ class WalletItem {
+ +Guid Id
+ +CurrencyType Currency
+ +decimal Balance
+ +decimal HeldBalance
+ +decimal AvailableBalance
+ }
+
+ class HoldItem {
+ +Guid Id
+ +decimal OriginalAmount
+ +decimal RemainingAmount
+ +decimal ExecutedAmount
+ +decimal ReleasedAmount
+ +HoldStatus Status
+ +string ReferenceType
+ +Guid ReferenceId
+ +Execute(amount, ref)
+ +Release(amount)
+ +Cancel()
}
class WalletTransaction {
+Guid Id
+TransactionType Type
- +Money Amount
- +Money BalanceAfter
+ +decimal Amount
+ +CurrencyType Currency
+ +decimal BalanceAfter
+string Description
+DateTime CreatedAt
}
- class Money {
- +decimal Amount
- +string Currency
+ class CurrencyType {
+ +int Id
+ +string Name
+ +decimal BaseExchangeRate
+ +VND
+ +USD
+ +PPoint
+ +GetExchangeRateTo(currency)
+ +ConvertTo(amount, currency)
}
+ Wallet "1" --> "*" WalletItem
+ Wallet "1" --> "*" HoldItem
Wallet "1" --> "*" WalletTransaction
- Wallet --> Money
+ WalletItem --> CurrencyType
+ HoldItem --> CurrencyType
```
### PointAccount Aggregate
@@ -85,9 +150,9 @@ classDiagram
class PointAccount {
+Guid Id
+Guid UserId
- +int TotalPoints
- +int AvailablePoints
- +int PendingPoints
+ +long TotalPoints
+ +long AvailablePoints
+ +long PendingPoints
+EarnPoints(points, source, desc, expires)
+SpendPoints(points, source, desc)
+AdjustPoints(points, source, desc)
@@ -96,8 +161,9 @@ classDiagram
class PointTransaction {
+Guid Id
+PointTransactionType Type
- +int Points
- +int BalanceAfter
+ +long Points
+ +long BalanceAfter
+ +string Source
+DateTime? ExpiresAt
}
@@ -111,52 +177,113 @@ classDiagram
| Table | Description |
|-------|-------------|
| `Wallets` | User wallet accounts |
+| `WalletItems` | Currency balances per wallet |
| `WalletTransactions` | Wallet transaction history |
+| `HoldItems` | Escrow holds |
| `PointAccounts` | User point accounts |
| `PointTransactions` | Point transaction history |
### Key Indexes
- `IX_Wallets_UserId` - Fast lookup by user
+- `IX_WalletItems_WalletId_CurrencyTypeId` - Balance by currency
- `IX_WalletTransactions_WalletId` - Transaction history
+- `IX_HoldItems_WalletId_Status` - Active holds
- `IX_PointAccounts_UserId` - Fast lookup by user
## API Flow
### Deposit Flow
-```
-1. Client → POST /api/v1/wallets/deposit
-2. Controller → DepositCommand (MediatR)
-3. CommandHandler → Validate amount
-4. Handler → wallet.Deposit(amount)
-5. Domain → Create WalletTransaction + Domain Event
-6. Infrastructure → Save to database
-7. Response → Updated balance
+```mermaid
+sequenceDiagram
+ participant C as Client
+ participant API as Controller
+ participant H as Handler
+ participant W as Wallet
+ participant DB as Database
+
+ C->>API: POST /wallets/{userId}/deposit
+ API->>H: DepositCommand
+ H->>W: wallet.Deposit(amount)
+ W->>W: Create Transaction
+ W->>W: Raise DomainEvent
+ H->>DB: SaveChanges
+ DB-->>API: Success
+ API-->>C: Updated Balance
```
-### Transfer Flow
+### Escrow Flow
+```mermaid
+sequenceDiagram
+ participant C as Client
+ participant API as Controller
+ participant W as Wallet
+ participant H as HoldItem
+
+ C->>API: POST /holds (Create)
+ API->>W: wallet.Hold(amount)
+ W->>H: Create HoldItem
+ W-->>C: HoldId
+
+ C->>API: POST /holds/{id}/execute
+ API->>W: wallet.ExecuteHold(id, amount)
+ W->>H: hold.Execute(amount)
+ H-->>C: Executed Amount
+
+ C->>API: POST /holds/{id}/release
+ API->>W: wallet.ReleaseHold(id, amount)
+ W->>H: hold.Release(amount)
+ H-->>C: Released Amount
```
-1. Client → POST /api/v1/wallets/transfer
-2. Controller → TransferCommand
-3. Handler → Get source wallet
-4. Handler → Get target wallet
-5. Domain → source.Withdraw() + target.Deposit()
-6. Infrastructure → Transactional save
-7. Response → Transfer confirmation
+
+### Currency Exchange Flow
+
+```mermaid
+sequenceDiagram
+ participant C as Client
+ participant API as Controller
+ participant W as Wallet
+ participant CT as CurrencyType
+
+ C->>API: POST /exchange
+ API->>W: wallet.Exchange(100 USD, VND)
+ W->>CT: GetExchangeRate(USD → VND)
+ CT-->>W: Rate = 25000
+ W->>W: Withdraw 100 USD
+ W->>W: Deposit 2,500,000 VND
+ W->>W: Raise WalletExchangedEvent
+ W-->>C: Exchange Result
```
+## Domain Events
+
+| Event | Trigger | Data |
+|-------|---------|------|
+| `WalletCreatedDomainEvent` | Wallet creation | WalletId, UserId |
+| `WalletBalanceChangedDomainEvent` | Deposit/Withdraw | WalletId, Amount, Type |
+| `WalletExchangedDomainEvent` | Currency exchange | FromCurrency, ToCurrency, Rate |
+| `EscrowHeldDomainEvent` | Hold creation | HoldId, Amount, RefType |
+| `EscrowExecutedDomainEvent` | Hold execution | HoldId, Amount |
+| `EscrowReleasedDomainEvent` | Hold release | HoldId, Amount |
+| `PointsEarnedDomainEvent` | Points earned | AccountId, Points |
+| `PointsSpentDomainEvent` | Points spent | AccountId, Points |
+
## Inter-Service Communication
### IAM Service Integration
-```
-Wallet Service ──────► IAM Service
- │
- ▼
- User Validation
- JWT Verification
+```mermaid
+flowchart LR
+ WS["🔐 Wallet Service"] --> IAM["👤 IAM Service"]
+ IAM --> UV["User Validation"]
+ IAM --> JV["JWT Verification"]
+
+ style WS fill:#3498DB,color:#ECF0F1,stroke:#2980B9,stroke-width:2px
+ style IAM fill:#8E44AD,color:#ECF0F1,stroke:#7D3C98,stroke-width:2px
+ style UV fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px
+ style JV fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px
```
### Authentication Flow
@@ -198,7 +325,7 @@ wallet-service:
### Authorization
- User can only access own wallet
-- Admin endpoints for system operations
+- Admin endpoints require Admin/SuperAdmin role
### Data Protection
- All amounts stored with precision
diff --git a/services/wallet-service-net/docs/en/README.md b/services/wallet-service-net/docs/en/README.md
index 331915b0..3a8d5a97 100644
--- a/services/wallet-service-net/docs/en/README.md
+++ b/services/wallet-service-net/docs/en/README.md
@@ -1,7 +1,6 @@
# Wallet Service .NET
-> **EN**: Wallet and Point Account management service for GoodGo Platform.
-> **VI**: Dịch vụ quản lý Ví và Tài khoản Điểm cho GoodGo Platform.
+Wallet and Point Account management service for GoodGo Platform.
## Overview
@@ -11,7 +10,9 @@ The Wallet Service provides comprehensive wallet and loyalty points management w
- **Escrow Module** - Hold, commit, and release funds (for Promotion Service)
- **Point Account** - Earn, spend, and track loyalty points
- **Transaction History** - Full audit trail of all transactions
-- **Multi-Currency Support** - Default VND with currency support
+- **Multi-Currency Support** - VND, USD, PPoint with exchange capabilities
+- **Currency Exchange** - Convert between currencies with configurable rates
+- **Admin Backoffice** - Full admin APIs for wallet/points management
- **Domain-Driven Design** - Clean Architecture with CQRS pattern
## Tech Stack
@@ -64,33 +65,53 @@ dotnet run --project src/WalletService.API
| Method | Endpoint | Description |
|--------|----------|-------------|
| `POST` | `/api/v1/wallets` | Create new wallet |
-| `GET` | `/api/v1/wallets/me` | Get current user's wallet |
-| `GET` | `/api/v1/wallets/{id}` | Get wallet by ID |
-| `POST` | `/api/v1/wallets/deposit` | Deposit funds |
-| `POST` | `/api/v1/wallets/withdraw` | Withdraw funds |
-| `POST` | `/api/v1/wallets/transfer` | Transfer between wallets |
-| `POST` | `/api/v1/wallets/{id}/freeze` | Freeze wallet |
-| `POST` | `/api/v1/wallets/{id}/unfreeze` | Unfreeze wallet |
-| `GET` | `/api/v1/wallets/transactions` | Get transaction history |
+| `GET` | `/api/v1/wallets/{userId}` | Get wallet by user ID |
+| `POST` | `/api/v1/wallets/{userId}/deposit` | Deposit funds |
+| `POST` | `/api/v1/wallets/{userId}/withdraw` | Withdraw funds |
+| `GET` | `/api/v1/wallets/{userId}/transactions` | Get transaction history |
-### Escrow APIs (New)
+### Escrow/Hold APIs
| Method | Endpoint | Description |
|--------|----------|-------------|
-| `POST` | `/api/v1/wallets/{walletId}/holds` | Create a hold |
-| `POST` | `/api/v1/wallets/{walletId}/holds/{holdId}/execute` | Execute/Commit hold (deduct funds) |
+| `POST` | `/api/v1/wallets/{walletId}/holds` | Create escrow hold |
+| `GET` | `/api/v1/wallets/{walletId}/holds/{holdId}` | Get hold details |
+| `POST` | `/api/v1/wallets/{walletId}/holds/{holdId}/execute` | Execute hold (deduct funds) |
| `POST` | `/api/v1/wallets/{walletId}/holds/{holdId}/release` | Release hold (return funds) |
| `POST` | `/api/v1/wallets/{walletId}/holds/{holdId}/cancel` | Cancel hold |
-| `GET` | `/api/v1/wallets/{walletId}/holds/{holdId}` | Get hold details |
### Points APIs
| Method | Endpoint | Description |
|--------|----------|-------------|
-| `GET` | `/api/v1/points/me` | Get current user's points |
-| `POST` | `/api/v1/points/earn` | Earn points |
-| `POST` | `/api/v1/points/spend` | Spend points |
-| `GET` | `/api/v1/points/transactions` | Get point transactions |
+| `POST` | `/api/v1/points` | Create point account |
+| `GET` | `/api/v1/points/{userId}` | Get point account |
+| `POST` | `/api/v1/points/{userId}/earn` | Earn points |
+| `POST` | `/api/v1/points/{userId}/spend` | Spend points |
+| `GET` | `/api/v1/points/{userId}/transactions` | Get point transactions |
+
+### Admin Wallet APIs
+
+| Method | Endpoint | Description |
+|--------|----------|-------------|
+| `GET` | `/api/v1/admin/wallets` | Get all wallets (paginated) |
+| `GET` | `/api/v1/admin/wallets/{walletId}` | Get wallet details |
+| `POST` | `/api/v1/admin/wallets/{walletId}/freeze` | Freeze wallet |
+| `POST` | `/api/v1/admin/wallets/{walletId}/unfreeze` | Unfreeze wallet |
+| `POST` | `/api/v1/admin/wallets/{walletId}/adjust` | Adjust balance |
+| `GET` | `/api/v1/admin/wallets/statistics` | Get wallet statistics |
+| `GET` | `/api/v1/admin/wallets/search` | Search wallets |
+
+### Admin Points APIs
+
+| Method | Endpoint | Description |
+|--------|----------|-------------|
+| `GET` | `/api/v1/admin/points` | Get all point accounts |
+| `GET` | `/api/v1/admin/points/{accountId}` | Get point account details |
+| `POST` | `/api/v1/admin/points/{accountId}/adjust` | Adjust points |
+| `POST` | `/api/v1/admin/points/{accountId}/bonus` | Grant bonus points |
+| `GET` | `/api/v1/admin/points/statistics` | Get points statistics |
+| `GET` | `/api/v1/admin/points/search` | Search point accounts |
### Health Endpoints
@@ -100,6 +121,34 @@ dotnet run --project src/WalletService.API
| `/health/live` | Liveness probe (K8s) |
| `/health/ready` | Readiness probe (K8s) |
+## Multi-Currency Support
+
+Wallet supports multiple currency types with exchange capabilities:
+
+| Currency | Code | Base Rate to VND |
+|----------|------|------------------|
+| Vietnamese Dong | `VND` | 1 |
+| US Dollar | `USD` | 25,000 |
+| Loyalty Points | `PPoint` | 1,000 |
+
+### Currency Exchange
+
+```csharp
+// Exchange USD to VND
+wallet.Exchange(
+ fromAmount: 100m,
+ fromCurrency: CurrencyType.USD,
+ toCurrency: CurrencyType.VND
+); // Returns 2,500,000 VND
+
+// Exchange PPoints to VND
+wallet.Exchange(
+ fromAmount: 50m,
+ fromCurrency: CurrencyType.PPoint,
+ toCurrency: CurrencyType.VND
+); // Returns 50,000 VND
+```
+
## Project Structure
```
@@ -107,13 +156,14 @@ wallet-service-net/
├── src/
│ ├── WalletService.API/ # API Layer
│ │ ├── Controllers/ # REST endpoints
+│ │ │ └── Admin/ # Admin endpoints
│ │ └── Application/ # Commands & Queries
│ │ ├── Commands/ # Write operations
│ │ └── Queries/ # Read operations
│ │
│ ├── WalletService.Domain/ # Domain Layer
│ │ ├── AggregatesModel/
-│ │ │ ├── WalletAggregate/ # Wallet, Transaction, Money
+│ │ │ ├── WalletAggregate/ # Wallet, HoldItem, CurrencyType
│ │ │ └── PointAccountAggregate/ # Points, PointTransaction
│ │ ├── Events/ # Domain events
│ │ └── Exceptions/ # Domain exceptions
@@ -140,24 +190,26 @@ wallet-service-net/
```csharp
// Create wallet
-var wallet = new Wallet(userId, "VND");
+var wallet = new Wallet(userId, CurrencyType.VND);
// Deposit funds
-wallet.Deposit(new Money(1000000m, "VND"), "Salary", "REF001");
+wallet.Deposit(1000000m, CurrencyType.VND, "Salary", "REF001");
// Withdraw funds
-wallet.Withdraw(new Money(500000m, "VND"), "Shopping", "REF002");
+wallet.Withdraw(500000m, CurrencyType.VND, "Shopping", "REF002");
// Freeze/Unfreeze
wallet.Freeze();
wallet.Unfreeze();
-// Escrow
+// Escrow operations
var hold = wallet.Hold(100000m, CurrencyType.VND, "CAMPAIGN", campaignId, "Hold for campaign");
wallet.ExecuteHold(hold.Id, 50000m, "ORDER123"); // Commit 50k
wallet.ReleaseHold(hold.Id, 50000m); // Return 50k
wallet.CancelHold(hold.Id); // Cancel remaining
+// Currency exchange
+wallet.Exchange(100m, CurrencyType.USD, CurrencyType.VND);
```
### Point Account Aggregate
diff --git a/services/wallet-service-net/docs/vi/ARCHITECTURE.md b/services/wallet-service-net/docs/vi/ARCHITECTURE.md
index 254f17a5..00f85146 100644
--- a/services/wallet-service-net/docs/vi/ARCHITECTURE.md
+++ b/services/wallet-service-net/docs/vi/ARCHITECTURE.md
@@ -4,18 +4,36 @@
Wallet Service quản lý ví điện tử và tài khoản điểm thưởng cho GoodGo Platform.
-```
-┌─────────────────────────────────────────────────────────────┐
-│ Wallet Service │
-├─────────────────────────────────────────────────────────────┤
-│ API Layer (Controllers, CQRS) │
-├─────────────────────────────────────────────────────────────┤
-│ Domain Layer (Wallet, PointAccount Aggregates) │
-├─────────────────────────────────────────────────────────────┤
-│ Infrastructure Layer (EF Core, Repositories) │
-├─────────────────────────────────────────────────────────────┤
-│ PostgreSQL Database │
-└─────────────────────────────────────────────────────────────┘
+```mermaid
+flowchart TB
+ subgraph API["🌐 API Layer"]
+ Controllers["Controllers"]
+ Commands["Commands"]
+ Queries["Queries"]
+ end
+
+ subgraph Domain["💎 Domain Layer"]
+ Wallet["Wallet Aggregate"]
+ PointAccount["PointAccount Aggregate"]
+ end
+
+ subgraph Infra["⚙️ Infrastructure Layer"]
+ Repos["Repositories"]
+ EF["EF Core"]
+ end
+
+ subgraph DB["💾 PostgreSQL"]
+ Tables["Tables"]
+ end
+
+ API --> Domain
+ Domain --> Infra
+ Infra --> DB
+
+ style API fill:#3498DB,color:#ECF0F1,stroke:#2980B9,stroke-width:3px
+ style Domain fill:#8E44AD,color:#ECF0F1,stroke:#7D3C98,stroke-width:2px
+ style Infra fill:#E67E22,color:#ECF0F1,stroke:#D35400,stroke-width:2px
+ style DB fill:#34495E,color:#ECF0F1,stroke:#2C3E50,stroke-width:2px
```
## Các Pattern Kiến Trúc
@@ -23,22 +41,33 @@ Wallet Service quản lý ví điện tử và tài khoản điểm thưởng ch
### Domain-Driven Design (DDD)
- **Aggregates**: Wallet, PointAccount
-- **Entities**: WalletTransaction, PointTransaction
-- **Value Objects**: Money, PointBalance
-- **Domain Events**: WalletCreated, BalanceChanged, PointsEarned
+- **Entities**: WalletTransaction, PointTransaction, HoldItem, WalletItem
+- **Value Objects**: Money, CurrencyType
+- **Domain Events**: WalletCreated, BalanceChanged, PointsEarned, EscrowHeld, EscrowExecuted
### CQRS Pattern
-```
-Commands (Ghi) Queries (Đọc)
- │ │
- ▼ ▼
-CreateWallet GetWalletQuery
-DepositCommand GetTransactionsQuery
-WithdrawCommand GetPointAccountQuery
-TransferCommand GetBalanceSummaryQuery
-EarnPointsCommand
-SpendPointsCommand
+```mermaid
+flowchart LR
+ subgraph Commands["Commands (Ghi)"]
+ C1["CreateWallet"]
+ C2["Deposit/Withdraw"]
+ C3["Exchange"]
+ C4["Hold/Execute/Release"]
+ C5["EarnPoints/SpendPoints"]
+ C6["Admin Commands"]
+ end
+
+ subgraph Queries["Queries (Đọc)"]
+ Q1["GetWallet"]
+ Q2["GetTransactions"]
+ Q3["GetPointAccount"]
+ Q4["GetStatistics"]
+ Q5["Admin Queries"]
+ end
+
+ style Commands fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px
+ style Queries fill:#3498DB,color:#ECF0F1,stroke:#2980B9,stroke-width:2px
```
## Domain Model
@@ -50,32 +79,68 @@ classDiagram
class Wallet {
+Guid Id
+Guid UserId
- +Money Balance
+WalletStatus Status
- +List~WalletTransaction~ Transactions
- +Deposit(Money, description, ref)
- +Withdraw(Money, description, ref)
+ +CurrencyType DefaultCurrency
+ +List~WalletItem~ Items
+ +List~HoldItem~ Holds
+ +Deposit(amount, currency, desc, ref)
+ +Withdraw(amount, currency, desc, ref)
+ +Exchange(fromAmount, fromCurrency, toCurrency)
+ +Hold(amount, currency, refType, refId, desc)
+ +ExecuteHold(holdId, amount, ref)
+ +ReleaseHold(holdId, amount)
+Freeze()
+Unfreeze()
- +Close()
+ }
+
+ class WalletItem {
+ +Guid Id
+ +CurrencyType Currency
+ +decimal Balance
+ +decimal HeldBalance
+ +decimal AvailableBalance
+ }
+
+ class HoldItem {
+ +Guid Id
+ +decimal OriginalAmount
+ +decimal RemainingAmount
+ +decimal ExecutedAmount
+ +decimal ReleasedAmount
+ +HoldStatus Status
+ +string ReferenceType
+ +Guid ReferenceId
+ +Execute(amount, ref)
+ +Release(amount)
+ +Cancel()
}
class WalletTransaction {
+Guid Id
+TransactionType Type
- +Money Amount
- +Money BalanceAfter
+ +decimal Amount
+ +CurrencyType Currency
+ +decimal BalanceAfter
+string Description
+DateTime CreatedAt
}
- class Money {
- +decimal Amount
- +string Currency
+ class CurrencyType {
+ +int Id
+ +string Name
+ +decimal BaseExchangeRate
+ +VND
+ +USD
+ +PPoint
+ +GetExchangeRateTo(currency)
+ +ConvertTo(amount, currency)
}
+ Wallet "1" --> "*" WalletItem
+ Wallet "1" --> "*" HoldItem
Wallet "1" --> "*" WalletTransaction
- Wallet --> Money
+ WalletItem --> CurrencyType
+ HoldItem --> CurrencyType
```
### PointAccount Aggregate
@@ -85,9 +150,9 @@ classDiagram
class PointAccount {
+Guid Id
+Guid UserId
- +int TotalPoints
- +int AvailablePoints
- +int PendingPoints
+ +long TotalPoints
+ +long AvailablePoints
+ +long PendingPoints
+EarnPoints(points, source, desc, expires)
+SpendPoints(points, source, desc)
+AdjustPoints(points, source, desc)
@@ -96,8 +161,9 @@ classDiagram
class PointTransaction {
+Guid Id
+PointTransactionType Type
- +int Points
- +int BalanceAfter
+ +long Points
+ +long BalanceAfter
+ +string Source
+DateTime? ExpiresAt
}
@@ -111,52 +177,113 @@ classDiagram
| Bảng | Mô Tả |
|------|-------|
| `Wallets` | Tài khoản ví người dùng |
+| `WalletItems` | Số dư tiền tệ mỗi ví |
| `WalletTransactions` | Lịch sử giao dịch ví |
+| `HoldItems` | Các lệnh ký quỹ |
| `PointAccounts` | Tài khoản điểm người dùng |
| `PointTransactions` | Lịch sử giao dịch điểm |
### Indexes Chính
- `IX_Wallets_UserId` - Tra cứu theo user
+- `IX_WalletItems_WalletId_CurrencyTypeId` - Số dư theo tiền tệ
- `IX_WalletTransactions_WalletId` - Lịch sử giao dịch
+- `IX_HoldItems_WalletId_Status` - Các hold đang hoạt động
- `IX_PointAccounts_UserId` - Tra cứu theo user
## API Flow
### Luồng Nạp Tiền
-```
-1. Client → POST /api/v1/wallets/deposit
-2. Controller → DepositCommand (MediatR)
-3. CommandHandler → Validate số tiền
-4. Handler → wallet.Deposit(amount)
-5. Domain → Tạo WalletTransaction + Domain Event
-6. Infrastructure → Lưu database
-7. Response → Số dư cập nhật
+```mermaid
+sequenceDiagram
+ participant C as Client
+ participant API as Controller
+ participant H as Handler
+ participant W as Wallet
+ participant DB as Database
+
+ C->>API: POST /wallets/{userId}/deposit
+ API->>H: DepositCommand
+ H->>W: wallet.Deposit(amount)
+ W->>W: Tạo Transaction
+ W->>W: Raise DomainEvent
+ H->>DB: SaveChanges
+ DB-->>API: Thành công
+ API-->>C: Số dư cập nhật
```
-### Luồng Chuyển Khoản
+### Luồng Escrow
+```mermaid
+sequenceDiagram
+ participant C as Client
+ participant API as Controller
+ participant W as Wallet
+ participant H as HoldItem
+
+ C->>API: POST /holds (Tạo)
+ API->>W: wallet.Hold(amount)
+ W->>H: Tạo HoldItem
+ W-->>C: HoldId
+
+ C->>API: POST /holds/{id}/execute
+ API->>W: wallet.ExecuteHold(id, amount)
+ W->>H: hold.Execute(amount)
+ H-->>C: Số tiền đã thực thi
+
+ C->>API: POST /holds/{id}/release
+ API->>W: wallet.ReleaseHold(id, amount)
+ W->>H: hold.Release(amount)
+ H-->>C: Số tiền đã giải phóng
```
-1. Client → POST /api/v1/wallets/transfer
-2. Controller → TransferCommand
-3. Handler → Lấy ví nguồn
-4. Handler → Lấy ví đích
-5. Domain → source.Withdraw() + target.Deposit()
-6. Infrastructure → Lưu transactional
-7. Response → Xác nhận chuyển khoản
+
+### Luồng Quy Đổi Tiền Tệ
+
+```mermaid
+sequenceDiagram
+ participant C as Client
+ participant API as Controller
+ participant W as Wallet
+ participant CT as CurrencyType
+
+ C->>API: POST /exchange
+ API->>W: wallet.Exchange(100 USD, VND)
+ W->>CT: GetExchangeRate(USD → VND)
+ CT-->>W: Tỷ giá = 25000
+ W->>W: Rút 100 USD
+ W->>W: Nạp 2.500.000 VND
+ W->>W: Raise WalletExchangedEvent
+ W-->>C: Kết quả quy đổi
```
+## Domain Events
+
+| Event | Kích Hoạt | Dữ Liệu |
+|-------|-----------|---------|
+| `WalletCreatedDomainEvent` | Tạo ví | WalletId, UserId |
+| `WalletBalanceChangedDomainEvent` | Nạp/Rút | WalletId, Amount, Type |
+| `WalletExchangedDomainEvent` | Quy đổi tiền tệ | FromCurrency, ToCurrency, Rate |
+| `EscrowHeldDomainEvent` | Tạo hold | HoldId, Amount, RefType |
+| `EscrowExecutedDomainEvent` | Thực thi hold | HoldId, Amount |
+| `EscrowReleasedDomainEvent` | Giải phóng hold | HoldId, Amount |
+| `PointsEarnedDomainEvent` | Tích điểm | AccountId, Points |
+| `PointsSpentDomainEvent` | Tiêu điểm | AccountId, Points |
+
## Tích Hợp Giữa Các Service
### Tích Hợp IAM Service
-```
-Wallet Service ──────► IAM Service
- │
- ▼
- Xác thực User
- Kiểm tra JWT
+```mermaid
+flowchart LR
+ WS["🔐 Wallet Service"] --> IAM["👤 IAM Service"]
+ IAM --> UV["Xác thực User"]
+ IAM --> JV["Kiểm tra JWT"]
+
+ style WS fill:#3498DB,color:#ECF0F1,stroke:#2980B9,stroke-width:2px
+ style IAM fill:#8E44AD,color:#ECF0F1,stroke:#7D3C98,stroke-width:2px
+ style UV fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px
+ style JV fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px
```
### Luồng Xác Thực
@@ -198,7 +325,7 @@ wallet-service:
### Phân Quyền
- User chỉ truy cập được ví của mình
-- Admin endpoints cho system operations
+- Admin endpoints yêu cầu role Admin/SuperAdmin
### Bảo Vệ Dữ Liệu
- Lưu số tiền với độ chính xác cao
diff --git a/services/wallet-service-net/docs/vi/README.md b/services/wallet-service-net/docs/vi/README.md
index 040f2cd2..e406138e 100644
--- a/services/wallet-service-net/docs/vi/README.md
+++ b/services/wallet-service-net/docs/vi/README.md
@@ -1,7 +1,6 @@
# Wallet Service .NET
-> **EN**: Wallet and Point Account management service for GoodGo Platform.
-> **VI**: Dịch vụ quản lý Ví và Tài khoản Điểm cho GoodGo Platform.
+Dịch vụ quản lý Ví và Tài khoản Điểm cho GoodGo Platform.
## Tổng Quan
@@ -11,7 +10,9 @@ Wallet Service cung cấp quản lý ví và điểm thưởng toàn diện:
- **Escrow Module** - Ký quỹ, cam kết và giải phóng tiền (cho Promotion Service)
- **Tài Khoản Điểm** - Tích, tiêu và theo dõi điểm thưởng
- **Lịch Sử Giao Dịch** - Audit trail đầy đủ các giao dịch
-- **Hỗ Trợ Đa Tiền Tệ** - Mặc định VND với hỗ trợ đa tiền tệ
+- **Hỗ Trợ Đa Tiền Tệ** - VND, USD, PPoint với khả năng quy đổi
+- **Quy Đổi Tiền Tệ** - Chuyển đổi giữa các loại tiền với tỷ giá cấu hình
+- **Admin Backoffice** - API quản trị đầy đủ cho ví/điểm
- **Domain-Driven Design** - Clean Architecture với CQRS pattern
## Tech Stack
@@ -64,33 +65,53 @@ dotnet run --project src/WalletService.API
| Method | Endpoint | Mô Tả |
|--------|----------|-------|
| `POST` | `/api/v1/wallets` | Tạo ví mới |
-| `GET` | `/api/v1/wallets/me` | Lấy ví của người dùng hiện tại |
-| `GET` | `/api/v1/wallets/{id}` | Lấy ví theo ID |
-| `POST` | `/api/v1/wallets/deposit` | Nạp tiền vào ví |
-| `POST` | `/api/v1/wallets/withdraw` | Rút tiền từ ví |
-| `POST` | `/api/v1/wallets/transfer` | Chuyển tiền giữa các ví |
-| `POST` | `/api/v1/wallets/{id}/freeze` | Đóng băng ví |
-| `POST` | `/api/v1/wallets/{id}/unfreeze` | Mở đóng băng ví |
-| `GET` | `/api/v1/wallets/transactions` | Lấy lịch sử giao dịch |
+| `GET` | `/api/v1/wallets/{userId}` | Lấy ví theo user ID |
+| `POST` | `/api/v1/wallets/{userId}/deposit` | Nạp tiền vào ví |
+| `POST` | `/api/v1/wallets/{userId}/withdraw` | Rút tiền từ ví |
+| `GET` | `/api/v1/wallets/{userId}/transactions` | Lấy lịch sử giao dịch |
-### Escrow APIs (New)
+### Escrow/Hold APIs
| Method | Endpoint | Mô Tả |
|--------|----------|-------|
-| `POST` | `/api/v1/wallets/{walletId}/holds` | Tạo lệnh giữ tiền (Hold) |
-| `POST` | `/api/v1/wallets/{walletId}/holds/{holdId}/execute` | Thực thi lệnh giữ tiền (trừ tiền thực) |
-| `POST` | `/api/v1/wallets/{walletId}/holds/{holdId}/release` | Giải phóng tiền giữ (trả lại ví) |
-| `POST` | `/api/v1/wallets/{walletId}/holds/{holdId}/cancel` | Hủy lệnh giữ tiền |
-| `GET` | `/api/v1/wallets/{walletId}/holds/{holdId}` | Lấy thông tin lệnh giữ tiền |
+| `POST` | `/api/v1/wallets/{walletId}/holds` | Tạo lệnh ký quỹ |
+| `GET` | `/api/v1/wallets/{walletId}/holds/{holdId}` | Lấy thông tin ký quỹ |
+| `POST` | `/api/v1/wallets/{walletId}/holds/{holdId}/execute` | Thực thi (trừ tiền) |
+| `POST` | `/api/v1/wallets/{walletId}/holds/{holdId}/release` | Giải phóng (trả lại) |
+| `POST` | `/api/v1/wallets/{walletId}/holds/{holdId}/cancel` | Hủy ký quỹ |
### Points APIs
| Method | Endpoint | Mô Tả |
|--------|----------|-------|
-| `GET` | `/api/v1/points/me` | Lấy điểm của người dùng |
-| `POST` | `/api/v1/points/earn` | Tích điểm |
-| `POST` | `/api/v1/points/spend` | Tiêu điểm |
-| `GET` | `/api/v1/points/transactions` | Lấy lịch sử điểm |
+| `POST` | `/api/v1/points` | Tạo tài khoản điểm |
+| `GET` | `/api/v1/points/{userId}` | Lấy tài khoản điểm |
+| `POST` | `/api/v1/points/{userId}/earn` | Tích điểm |
+| `POST` | `/api/v1/points/{userId}/spend` | Tiêu điểm |
+| `GET` | `/api/v1/points/{userId}/transactions` | Lấy lịch sử điểm |
+
+### Admin Wallet APIs
+
+| Method | Endpoint | Mô Tả |
+|--------|----------|-------|
+| `GET` | `/api/v1/admin/wallets` | Lấy tất cả ví (phân trang) |
+| `GET` | `/api/v1/admin/wallets/{walletId}` | Lấy chi tiết ví |
+| `POST` | `/api/v1/admin/wallets/{walletId}/freeze` | Đóng băng ví |
+| `POST` | `/api/v1/admin/wallets/{walletId}/unfreeze` | Mở băng ví |
+| `POST` | `/api/v1/admin/wallets/{walletId}/adjust` | Điều chỉnh số dư |
+| `GET` | `/api/v1/admin/wallets/statistics` | Thống kê ví |
+| `GET` | `/api/v1/admin/wallets/search` | Tìm kiếm ví |
+
+### Admin Points APIs
+
+| Method | Endpoint | Mô Tả |
+|--------|----------|-------|
+| `GET` | `/api/v1/admin/points` | Lấy tất cả tài khoản điểm |
+| `GET` | `/api/v1/admin/points/{accountId}` | Lấy chi tiết tài khoản |
+| `POST` | `/api/v1/admin/points/{accountId}/adjust` | Điều chỉnh điểm |
+| `POST` | `/api/v1/admin/points/{accountId}/bonus` | Tặng điểm thưởng |
+| `GET` | `/api/v1/admin/points/statistics` | Thống kê điểm |
+| `GET` | `/api/v1/admin/points/search` | Tìm kiếm tài khoản |
### Health Endpoints
@@ -100,6 +121,34 @@ dotnet run --project src/WalletService.API
| `/health/live` | Liveness probe (K8s) |
| `/health/ready` | Readiness probe (K8s) |
+## Hỗ Trợ Đa Tiền Tệ
+
+Ví hỗ trợ nhiều loại tiền tệ với khả năng quy đổi:
+
+| Tiền Tệ | Mã | Tỷ Giá Cơ Sở (VND) |
+|---------|----|--------------------|
+| Đồng Việt Nam | `VND` | 1 |
+| Đô La Mỹ | `USD` | 25.000 |
+| Điểm Thưởng | `PPoint` | 1.000 |
+
+### Quy Đổi Tiền Tệ
+
+```csharp
+// Quy đổi USD sang VND
+wallet.Exchange(
+ fromAmount: 100m,
+ fromCurrency: CurrencyType.USD,
+ toCurrency: CurrencyType.VND
+); // Trả về 2.500.000 VND
+
+// Quy đổi PPoints sang VND
+wallet.Exchange(
+ fromAmount: 50m,
+ fromCurrency: CurrencyType.PPoint,
+ toCurrency: CurrencyType.VND
+); // Trả về 50.000 VND
+```
+
## Cấu Trúc Dự Án
```
@@ -107,13 +156,14 @@ wallet-service-net/
├── src/
│ ├── WalletService.API/ # API Layer
│ │ ├── Controllers/ # REST endpoints
+│ │ │ └── Admin/ # Admin endpoints
│ │ └── Application/ # Commands & Queries
│ │ ├── Commands/ # Thao tác ghi
│ │ └── Queries/ # Thao tác đọc
│ │
│ ├── WalletService.Domain/ # Domain Layer
│ │ ├── AggregatesModel/
-│ │ │ ├── WalletAggregate/ # Wallet, Transaction, Money
+│ │ │ ├── WalletAggregate/ # Wallet, HoldItem, CurrencyType
│ │ │ └── PointAccountAggregate/ # Points, PointTransaction
│ │ ├── Events/ # Domain events
│ │ └── Exceptions/ # Domain exceptions
@@ -140,24 +190,26 @@ wallet-service-net/
```csharp
// Tạo ví
-var wallet = new Wallet(userId, "VND");
+var wallet = new Wallet(userId, CurrencyType.VND);
// Nạp tiền
-wallet.Deposit(new Money(1000000m, "VND"), "Lương", "REF001");
+wallet.Deposit(1000000m, CurrencyType.VND, "Lương", "REF001");
// Rút tiền
-wallet.Withdraw(new Money(500000m, "VND"), "Mua sắm", "REF002");
+wallet.Withdraw(500000m, CurrencyType.VND, "Mua sắm", "REF002");
-// Đóng băng/Mở đóng băng
+// Đóng băng/Mở băng
wallet.Freeze();
wallet.Unfreeze();
-// Escrow / Ký Quỹ
-var hold = wallet.Hold(100000m, CurrencyType.VND, "CAMPAIGN", campaignId, "Hold for campaign");
+// Thao tác Escrow
+var hold = wallet.Hold(100000m, CurrencyType.VND, "CAMPAIGN", campaignId, "Ký quỹ chiến dịch");
wallet.ExecuteHold(hold.Id, 50000m, "ORDER123"); // Thực thi 50k
wallet.ReleaseHold(hold.Id, 50000m); // Trả lại 50k
wallet.CancelHold(hold.Id); // Hủy phần còn lại
+// Quy đổi tiền tệ
+wallet.Exchange(100m, CurrencyType.USD, CurrencyType.VND);
```
### Point Account Aggregate