# System Architecture
```mermaid
graph TB
subgraph Client_Layer["👥 Client Layer"]
MobileApp[📱 Mobile App<br/>React Native/Flutter]
WebApp[🌐 Web App<br/>React/Next.js]
AdminUI[⚙️ Admin Portal<br/>React Admin]
end
subgraph API_Layer["🚪 API Layer"]
Gateway[API Gateway/BFF<br/>Kong/NGINX<br/>Authentication, Rate Limit<br/>Request Routing]
end
subgraph Service_Layer["⚡ Service Layer - Microservices"]
direction TB
AuthSvc[🔐 Auth Service<br/>Registration, Login,<br/>JWT Management]
KYCSvc[🪪 KYC Service<br/>eKYC Orchestration,<br/>Identity Verification]
AccountSvc[💼 Account Service<br/>Account Management,<br/>Balance Query]
LedgerSvc[📒 Ledger Service ⭐<br/>CORE<br/>Double-Entry Accounting,<br/>Immutable Ledger]
PaymentSvc[💰 Payment Service<br/>Deposit/Withdrawal,<br/>PSP Orchestration]
AccrualSvc[📊 Accrual Engine<br/>Daily Interest Calculation,<br/>EOD Job]
PortfolioSvc[📈 Portfolio Service<br/>Investment Management,<br/>Asset Allocation]
NotifSvc[📨 Notification Service<br/>Push/Email/SMS]
InsightSvc[💡 Insight Service<br/>Financial Health,<br/>Recommendations]
end
subgraph Data_Layer["💾 Data Layer - Database per Service"]
AuthDB[(🗄️ Auth DB<br/>PostgreSQL<br/>users,<br/>identities,<br/>sessions)]
KYCDB[(🗄️ KYC DB<br/>PostgreSQL<br/>kyc_profiles,<br/>documents)]
AccountDB[(🗄️ Account DB<br/>PostgreSQL<br/>accounts,<br/>balances,<br/>partners)]
LedgerDB[(🗄️ Ledger DB ⭐<br/>PostgreSQL<br/>CRITICAL<br/>ledger_entries,<br/>transactions)]
PaymentDB[(🗄️ Payment DB<br/>PostgreSQL<br/>deposits,<br/>withdrawals,<br/>bank_accounts)]
PortfolioDB[(🗄️ Portfolio DB<br/>PostgreSQL<br/>portfolios,<br/>orders,<br/>holdings)]
NotificationDB[(🗄️ Notification DB<br/>PostgreSQL<br/>templates,<br/>logs)]
Redis[(⚡ Redis Cache<br/>Session Cache<br/>Balance Cache<br/>OTP Cache)]
end
subgraph Message_Layer["📮 Message Infrastructure - Event-Driven Communication"]
MessageBroker[🔔 Message Broker<br/>RabbitMQ / Kafka<br/>Event Bus<br/>Pub/Sub Pattern]
end
subgraph External_Layer["🌐 External Systems"]
PSP[💳 VNPT ePay]
eKYC[🪪 VNPT eKYC]
Broker[📈 AAS Broker]
SMS[📨 SMS Gateway]
end
%% Client to Gateway
MobileApp -->|HTTPS/JSON| Gateway
WebApp -->|HTTPS/JSON| Gateway
AdminUI -->|HTTPS/JSON| Gateway
%% Gateway to Services
Gateway -->|Auth| AuthSvc
Gateway -->|KYC| KYCSvc
Gateway -->|Account| AccountSvc
Gateway -->|Payment| PaymentSvc
Gateway -->|Portfolio| PortfolioSvc
Gateway -->|Insight| InsightSvc
%% Service to Service (Critical Dependencies)
PaymentSvc -.->|Post Transaction| LedgerSvc
AccrualSvc -.->|Post Interest| LedgerSvc
AccountSvc -.->|Read Balance| LedgerSvc
PortfolioSvc -.->|Post Transfer| LedgerSvc
InsightSvc -.->|Read Entries| LedgerSvc
%% Service to Service (Synchronous - Direct API calls)
PaymentSvc -->|Validate Balance| AccountSvc
KYCSvc -->|Update Status| AuthSvc
%% Service to Message Broker (Asynchronous - Event-Driven)
PaymentSvc -.->|Publish Events| MessageBroker
KYCSvc -.->|Publish Events| MessageBroker
PortfolioSvc -.->|Publish Events| MessageBroker
AccrualSvc -.->|Publish Events| MessageBroker
%% Message Broker to Services (Subscribers)
MessageBroker -.->|Subscribe Events| NotifSvc
MessageBroker -.->|Subscribe Events| InsightSvc
MessageBroker -.->|Subscribe Events| AccountSvc
%% Service to Database
AuthSvc --> AuthDB
KYCSvc --> KYCDB
AccountSvc --> AccountDB
LedgerSvc --> LedgerDB
PaymentSvc --> PaymentDB
PortfolioSvc --> PortfolioDB
NotifSvc --> NotificationDB
AccrualSvc -.->|Read/Write| AccountDB
InsightSvc -.->|Read-only| LedgerDB
%% Service to Cache
AuthSvc -.->|Session & OTP| Redis
AccountSvc -.->|Balance Cache| Redis
PaymentSvc -.->|Idempotency Check| Redis
%% Service to External
PaymentSvc -->|Init Payment| PSP
PSP -.->|Webhook Result| PaymentSvc
KYCSvc -->|Init eKYC| eKYC
eKYC -.->|Webhook Result| KYCSvc
PortfolioSvc -->|Place Order| Broker
NotifSvc -->|Send OTP| SMS
%% Styling
style LedgerSvc fill:#ff6b6b,stroke:#c92a2a,stroke-width:4px,color:#fff
style LedgerDB fill:#ff6b6b,stroke:#c92a2a,stroke-width:4px,color:#fff
style Gateway fill:#4dabf7,stroke:#1971c2,stroke-width:3px
style MessageBroker fill:#ffd43b,stroke:#f59f00,stroke-width:3px
style Redis fill:#ffd43b,stroke:#f59f00,stroke-width:2px
style AuthDB fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
style KYCDB fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
style AccountDB fill:#e8f5e9,stroke:#388e3c,stroke-width:2px
style PaymentDB fill:#fff3e0,stroke:#f57c00,stroke-width:2px
style PortfolioDB fill:#fce4ec,stroke:#c2185b,stroke-width:2px
style NotificationDB fill:#e0f2f1,stroke:#00897b,stroke-width:2px
```
#### **WealthHup Platform Components**
**Client Layer**
Đây là tầng mà người dùng tương tác trực tiếp:
- **Mobile App**: Ứng dụng di động (iOS/Android) cho khách hàng
- **Web App**: Giao diện web cho khách hàng
- **Admin Portal**: Công cụ quản trị cho admin/ops
Tất cả đều giao tiếp với backend qua **HTTPS/JSON REST API**.
#### **API Layer**
**API Gateway/BFF (Backend For Frontend)** là điểm vào duy nhất:
1. **Authentication**: Verify JWT token
2. **Rate Limiting**: Giới hạn request (ví dụ: 100 req/minute)
3. **Request Routing**: Chuyển request đến service phù hợp
4. **Response Aggregation**: Gộp response từ nhiều service
5. **Logging & Monitoring**: Ghi log mọi request
#### **Service Layer Microservices)**
Mỗi service có **một trách nhiệm cụ thể** (Single Responsibility Principle):
##### 1. **Auth Service (Xác thực & Phân quyền)**
**Mục đích**: Quản lý người dùng và session
**Chức năng**:
- Đăng ký tài khoản (register)
- Đăng nhập (login)
- Phát hành JWT token
- Refresh token
- Quản lý session
**Database**: `AuthDB` (users, sessions, otp_logs, user_identities, user_audit_logs)
##### 2. **KYC Service (Xác thực danh tính)**
**Mục đích**: Orchestrate quy trình eKYC
**Chức năng**:
- Tạo session eKYC với VNPT
- Nhận webhook từ eKYC provider
- Kiểm tra kết quả C06
- Cập nhật trạng thái KYC của user
**Database**: `KYCDB` (kyc_profiles, kyc_documents, kyc_audit_logs)
##### 3. **Account Service (Quản lý tài khoản)**
**Mục đích**: Quản lý tài khoản logic của khách hàng
**Chức năng**:
- Tạo/Đóng tài khoản
- Truy vấn số dư (derived từ Ledger)
- Lock/Unlock balance
- Quản lý multi-partner accounts
**Database**: `AccountDB` (customers, accounts, account_balances, partners, accrual_logs)
**Quan trọng**:
- **KHÔNG được update balance trực tiếp**
- Balance = derived từ Ledger Service
- `account_balances` là materialized view (cache)
##### 4. **🌟 Ledger Service (CORE)**
**Mục đích**: Single Source of Truth cho mọi giao dịch tài chính
**Chức năng**:
- Post ledger transaction (double-entry)
- Reverse transaction (khi cần)
- Query ledger entries
- Calculate balance từ entries
**Database**: `LedgerDB` (ledger_accounts, ledger_transactions, ledger_entries, ledger_account_snapshots, ledger_revesal_log)
**Nguyên tắc thiết kế**:
- **Immutable**: Ledger entries KHÔNG BAO GIỜ được update/delete
- **Double-entry**: Luôn cân bằng (Σ DR = Σ CR)
- **Append-only**: Chỉ INSERT, không UPDATE
- **No dependencies**: Ledger KHÔNG gọi service khác
- **Internal only**: API chỉ dùng nội bộ, không expose ra ngoài
**Luồng dependency**:
```
Ledger Service (CORE)
▲
│ (Chỉ có chiều này, KHÔNG có chiều ngược lại)
│
├── Payment Service (post transaction)
├── Accrual Engine (post interest)
├── Portfolio Service (post transfer)
├── Account Service (read balance)
└── Insight Service (read entries)
```
##### 5. **Payment Service (Nạp/Rút tiền)**
**Mục đích**: Orchestrate quy trình nạp/rút tiền qua PSP
**Chức năng**:
- Init deposit (khởi tạo nạp tiền)
- Init withdrawal (khởi tạo rút tiền)
- Nhận webhook từ PSP
- Post transaction to Ledger
- Handle idempotency (webhook có thể retry nhiều lần)
**Database**: `PaymentDB` (deposits, withdrawals, bank_accounts, psp_webhooks, withdrawal_fee_config)
##### 6. **Accrual Engine (Tính lãi tự động)**
**Mục đích**: Tính và ghi nhận lãi hàng ngày
**Chức năng**:
- Chạy EOD (End of Day) job
- Tính lãi cho tất cả Flexible Accounts
- Post interest entries to Ledger
**Database**: Sử dụng `AccountDB` để lưu accrual logs
##### 7. **Portfolio Service (Quản lý danh mục đầu tư)**
**Mục đích**: Orchestrate việc đầu tư vào partner products
**Chức năng**:
- Hiển thị danh sách sản phẩm
- Validate risk profile
- Place order to broker
- Track investment performance
**Database**: `PortfolioDB` (portfolios, products, portfolio_hodings, orders, order_executions)
##### 8. **Notification Service (Thông báo)**
**Mục đích**: Gửi thông báo cho người dùng
**Chức năng**:
- Push notification
- Email
- SMS
**Database**: `NotificationDB` (notification_templates, notification_logs, user_notification_preferences)
**Characteristic**
- **Multi-channel**: PUSH, EMAIL, SMS
- **Template-based**: Tái sử dụng templates với variables
- **Retry mechanism**: Tự động retry khi failed (max 3 lần)
- **User preferences**: User có thể tắt/bật từng kênh
##### 9. **Insight Service**
**Mục đích**: Cung cấp insights tài chính cho người dùng
**Chức năng**:
- Financial Health score
- Asset allocation analysis
- Recommendations
#### **Message Infrastructure Layer (Event-Driven Communication)**
**Message Broker** (RabbitMQ / Apache Kafka / AWS SNS+SQS):
**Mục đích**:
- Decoupling services thông qua asynchronous events
- Pub/Sub pattern cho event-driven architecture
- Reliable message delivery với retry mechanism
**Use Cases trong WealthHup**:
| Event Type | Publisher | Subscribers | Purpose |
|------------|-----------|-------------|---------|
| **DepositSuccessEvent** | Payment Service | Notification Service, Insight Service | Thông báo nạp tiền thành công |
| **WithdrawalInitiatedEvent** | Payment Service | Notification Service | Thông báo rút tiền đang xử lý |
| **KYCApprovedEvent** | KYC Service | Account Service, Notification Service | Tạo account sau KYC |
| **InterestAccruedEvent** | Accrual Engine | Notification Service, Insight Service | Thông báo lãi được cộng |
| **OrderExecutedEvent** | Portfolio Service | Notification Service, Insight Service | Thông báo lệnh đã khớp |
| **BalanceChangedEvent** | Ledger Service | Account Service | Update balance cache |
#### **Data Layer (Tầng dữ liệu)**
**Database per Service Pattern**: Mỗi service có database riêng
**Lợi ích**:
- **Loose coupling**: Service không share database
- **Independent scaling**: Scale database theo nhu cầu
- **Technology flexibility**: Có thể dùng MongoDB cho một service, PostgreSQL cho service khác
**Redis Cache**:
- Session cache (JWT blacklist, OTP)
- Balance cache (giảm load cho Ledger)
- TTL: 5-15 phút
# Database design
```mermaid
erDiagram
%% ============================================================
%% AUTH DATABASE (auth_db) - UPDATED WITH IDENTITY ABSTRACTION
%% ============================================================
USERS {
uuid id PK
varchar password_hash "NULLABLE, for password-based login"
varchar display_name "NULLABLE"
varchar status "UNVERIFIED|PENDING_KYC|ACTIVE|SUSPENDED|CLOSED"
timestamp created_at
timestamp updated_at
timestamp last_login_at
}
USER_IDENTITIES {
uuid id PK
uuid user_id FK "NOT NULL"
varchar identity_type "PHONE|EMAIL|GOOGLE|FACEBOOK|APPLE"
varchar identity_value "NOT NULL"
boolean is_verified "DEFAULT FALSE"
boolean is_primary "DEFAULT FALSE"
jsonb provider_data "For OAuth providers"
timestamp verified_at
timestamp created_at
timestamp updated_at
}
AUTH_SESSIONS {
uuid id PK
uuid user_id FK "NOT NULL"
text access_token "NOT NULL"
text refresh_token UK "NOT NULL"
varchar device_id
varchar device_type "iOS|Android|Web"
inet ip_address
text user_agent
timestamp created_at
timestamp expired_at "NOT NULL"
timestamp last_used_at
boolean is_active "DEFAULT TRUE"
}
OTP_LOGS {
uuid id PK
uuid user_identity_id FK "NULLABLE"
varchar identity_type "PHONE|EMAIL"
varchar identity_value "NOT NULL"
varchar otp_code "NOT NULL"
varchar otp_hash "NOT NULL"
varchar purpose "REGISTER|LOGIN|RESET_PASSWORD|VERIFY_IDENTITY"
varchar status "SENT|VERIFIED|EXPIRED|FAILED"
int attempt_count "DEFAULT 0"
timestamp created_at
timestamp expired_at "NOT NULL"
timestamp verified_at
}
USER_AUDIT_LOGS {
uuid id PK
uuid user_id FK "NOT NULL"
varchar event_type "NOT NULL"
jsonb event_data
inet ip_address
text user_agent
uuid performed_by
timestamp created_at
}
%% ============================================================
%% LEDGER DATABASE (ledger_db) - CORE
%% ============================================================
LEDGER_ACCOUNTS {
uuid id PK
varchar code UK "NOT NULL"
varchar name "NOT NULL"
text description
varchar account_type "ASSET|LIABILITY|REVENUE|EXPENSE"
varchar owner_type "CUSTOMER|PARTNER|PLATFORM|SYSTEM"
uuid owner_id "NULLABLE"
varchar currency "DEFAULT VND"
boolean is_active "DEFAULT TRUE"
timestamp created_at
varchar created_by "NOT NULL"
}
LEDGER_TRANSACTIONS {
uuid id PK
varchar transaction_code UK "NOT NULL"
varchar transaction_type "DEPOSIT|WITHDRAWAL|INTEREST|FEE|TRANSFER|ADJUSTMENT"
varchar status "INIT|VALIDATED|POSTED|REVERSED"
varchar source_type "NOT NULL"
uuid source_id "NOT NULL"
uuid partner_id "NULLABLE"
bigint total_amount "NOT NULL"
varchar currency "DEFAULT VND"
date posting_date "NOT NULL"
timestamp created_at
varchar created_by "NOT NULL"
jsonb metadata
}
LEDGER_ENTRIES {
uuid id PK
uuid transaction_id FK "NOT NULL"
uuid ledger_account_id FK "NOT NULL"
bigint debit_amount "DEFAULT 0"
bigint credit_amount "DEFAULT 0"
varchar currency "DEFAULT VND"
varchar reference_id
text description
jsonb metadata
timestamp created_at "NOT NULL, IMMUTABLE"
}
LEDGER_REVERSAL_LOG {
uuid id PK
uuid original_transaction_id "FK, UK, NOT NULL"
uuid reversal_transaction_id FK "NOT NULL"
text reason "NOT NULL"
varchar performed_by "NOT NULL"
timestamp reversed_at
}
LEDGER_ACCOUNT_SNAPSHOTS {
uuid id PK
uuid ledger_account_id FK "NOT NULL"
date snapshot_date "NOT NULL"
bigint opening_balance "NOT NULL"
bigint closing_balance "NOT NULL"
bigint total_debit "NOT NULL"
bigint total_credit "NOT NULL"
int transaction_count "NOT NULL"
timestamp created_at
}
%% ============================================================
%% PAYMENT DATABASE (payment_db)
%% ============================================================
DEPOSITS {
uuid id PK
varchar deposit_code UK "NOT NULL"
uuid user_id "NOT NULL"
uuid account_id "NOT NULL"
bigint amount "NOT NULL, CHECK > 0"
varchar currency "DEFAULT VND"
varchar psp_provider "DEFAULT VNPT_EPAY"
varchar psp_txn_id UK "NULLABLE"
jsonb psp_request
jsonb psp_response
text psp_redirect_url
varchar status "INIT|PENDING|SUCCESS|FAILED|EXPIRED"
varchar idempotency_key UK "NOT NULL"
timestamp init_at
timestamp pending_at
timestamp completed_at
timestamp expired_at
timestamp created_at
jsonb metadata
}
WITHDRAWALS {
uuid id PK
varchar withdrawal_code UK "NOT NULL"
uuid user_id "NOT NULL"
uuid account_id "NOT NULL"
bigint amount "NOT NULL, CHECK > 0"
bigint fee "DEFAULT 0"
bigint net_amount "NOT NULL"
varchar currency "DEFAULT VND"
varchar speed "IMMEDIATE|DELAYED"
uuid bank_account_id FK "NOT NULL"
varchar psp_provider "DEFAULT VNPT_EPAY"
varchar psp_txn_id UK "NULLABLE"
jsonb psp_request
jsonb psp_response
varchar status "PENDING|PROCESSING|SUCCESS|FAILED|CANCELLED"
varchar idempotency_key UK "NOT NULL"
timestamp requested_at
timestamp processing_at
timestamp processed_at
timestamp estimated_arrival
timestamp created_at
jsonb metadata
}
BANK_ACCOUNTS {
uuid id PK
uuid user_id "NOT NULL"
varchar bank_code "NOT NULL"
varchar bank_name "NOT NULL"
varchar branch_name
varchar account_number "NOT NULL, ENCRYPTED"
varchar account_name "NOT NULL"
boolean is_verified "DEFAULT FALSE"
timestamp verified_at
varchar verified_by
boolean is_default "DEFAULT FALSE"
boolean is_active "DEFAULT TRUE"
timestamp created_at
timestamp updated_at
}
PSP_WEBHOOKS {
uuid id PK
varchar webhook_id UK "NOT NULL"
varchar webhook_type "NOT NULL"
varchar reference_type "NOT NULL"
uuid reference_id "NOT NULL"
jsonb payload "NOT NULL"
varchar signature "NOT NULL"
varchar status "RECEIVED|PROCESSING|PROCESSED|IGNORED|FAILED"
int retry_count "DEFAULT 0"
timestamp last_retry_at
timestamp received_at
timestamp processed_at
text error_message
}
WITHDRAWAL_FEE_CONFIG {
uuid id PK
varchar speed "NOT NULL"
varchar fee_type "FIXED|PERCENTAGE"
bigint fee_value "NOT NULL"
bigint min_fee
bigint max_fee
date effective_from "NOT NULL"
date effective_to
boolean is_active "DEFAULT TRUE"
timestamp created_at
varchar created_by "NOT NULL"
}
%% ============================================================
%% KYC DATABASE (kyc_db)
%% ============================================================
KYC_PROFILES {
uuid id PK
uuid user_id UK "NOT NULL"
varchar full_name "NOT NULL"
date date_of_birth "NOT NULL"
varchar id_number UK "NOT NULL"
varchar id_type "CCCD|PASSPORT"
date id_issued_date
varchar id_issued_place
varchar gender "MALE|FEMALE|OTHER"
varchar nationality "DEFAULT VN"
text address
varchar kyc_provider "VNPT_EKYC|FPT_EKYC"
varchar kyc_session_id UK "NULLABLE"
varchar status "PENDING|PROCESSING|APPROVED|REJECTED|EXPIRED"
text rejection_reason
timestamp submitted_at
timestamp approved_at
timestamp expired_at
varchar approved_by
jsonb kyc_data "ENCRYPTED"
timestamp created_at
timestamp updated_at
}
KYC_DOCUMENTS {
uuid id PK
uuid kyc_profile_id FK "NOT NULL"
varchar document_type "ID_FRONT|ID_BACK|SELFIE|FACE_VIDEO"
text document_url "NOT NULL"
varchar storage_path "NOT NULL"
bigint file_size
varchar mime_type
varchar verification_status "PENDING|VERIFIED|REJECTED"
float confidence_score
jsonb ocr_data
jsonb verification_result
timestamp uploaded_at
timestamp verified_at
}
KYC_AUDIT_LOGS {
uuid id PK
uuid kyc_profile_id FK "NOT NULL"
varchar event_type "NOT NULL"
varchar old_status
varchar new_status
text notes
jsonb event_data
varchar performed_by
timestamp created_at
}
%% ============================================================
%% ACCOUNT DATABASE (account_db)
%% ============================================================
CUSTOMERS {
uuid id PK
uuid user_id UK "NOT NULL"
varchar customer_code UK "NOT NULL"
varchar customer_type "INDIVIDUAL|CORPORATE"
varchar full_name "NOT NULL"
date date_of_birth
varchar phone "NOT NULL"
varchar email
text address
varchar risk_profile "CONSERVATIVE|MODERATE|AGGRESSIVE"
varchar status "ACTIVE|SUSPENDED|CLOSED"
timestamp created_at
timestamp updated_at
varchar created_by
}
ACCOUNTS {
uuid id PK
uuid customer_id FK "NOT NULL"
varchar account_code UK "NOT NULL"
varchar account_type "FLEXIBLE|TERM_DEPOSIT|INVESTMENT"
varchar currency "DEFAULT VND"
uuid partner_id "NULLABLE"
varchar status "ACTIVE|LOCKED|CLOSED"
timestamp opened_at
timestamp closed_at
varchar closed_reason
timestamp created_at
varchar created_by
}
ACCOUNT_BALANCES {
uuid id PK
uuid account_id UK, FK "NOT NULL"
bigint ledger_balance "NOT NULL, DEFAULT 0"
bigint available_balance "NOT NULL, DEFAULT 0"
bigint locked_balance "NOT NULL, DEFAULT 0"
bigint accrued_interest "NOT NULL, DEFAULT 0"
varchar currency "DEFAULT VND"
timestamp last_updated
timestamp synced_at
}
PARTNERS {
uuid id PK
varchar partner_code UK "NOT NULL"
varchar partner_name "NOT NULL"
varchar partner_type "BROKER|BANK|FINTECH"
text description
jsonb contact_info
jsonb api_config
boolean is_active "DEFAULT TRUE"
timestamp created_at
varchar created_by
}
ACCRUAL_LOGS {
uuid id PK
uuid account_id FK "NOT NULL"
date accrual_date "NOT NULL"
bigint principal_amount "NOT NULL"
float interest_rate "NOT NULL"
bigint interest_amount "NOT NULL"
varchar status "CALCULATED|POSTED|REVERSED"
uuid ledger_transaction_id "NULLABLE"
timestamp calculated_at
timestamp posted_at
varchar posted_by
}
%% ============================================================
%% PORTFOLIO DATABASE (portfolio_db)
%% ============================================================
PORTFOLIOS {
uuid id PK
uuid customer_id "NOT NULL"
uuid account_id FK "NOT NULL"
varchar portfolio_code UK "NOT NULL"
varchar portfolio_name "NOT NULL"
varchar strategy "BALANCED|GROWTH|INCOME|CONSERVATIVE"
varchar status "ACTIVE|SUSPENDED|CLOSED"
timestamp created_at
timestamp updated_at
}
PRODUCTS {
uuid id PK
varchar product_code UK "NOT NULL"
varchar product_name "NOT NULL"
varchar product_type "STOCK|BOND|FUND|GOLD|CRYPTO"
uuid partner_id FK "NOT NULL"
varchar currency "DEFAULT VND"
varchar risk_level "LOW|MEDIUM|HIGH"
bigint min_investment
text description
jsonb product_metadata
boolean is_active "DEFAULT TRUE"
timestamp created_at
}
PORTFOLIO_HOLDINGS {
uuid id PK
uuid portfolio_id FK "NOT NULL"
uuid product_id FK "NOT NULL"
float quantity "NOT NULL, DEFAULT 0"
bigint avg_cost_price "NOT NULL"
bigint market_value "NOT NULL"
bigint unrealized_pnl "NOT NULL"
timestamp last_updated
}
ORDERS {
uuid id PK
uuid portfolio_id FK "NOT NULL"
uuid product_id FK "NOT NULL"
varchar order_code UK "NOT NULL"
varchar order_type "BUY|SELL"
float quantity "NOT NULL"
bigint price "NOT NULL"
bigint total_amount "NOT NULL"
bigint fee "DEFAULT 0"
varchar status "PENDING|SUBMITTED|PARTIAL_FILLED|FILLED|CANCELLED|REJECTED"
uuid partner_id FK "NOT NULL"
varchar partner_order_id UK "NULLABLE"
jsonb partner_request
jsonb partner_response
timestamp submitted_at
timestamp filled_at
timestamp created_at
varchar created_by
}
ORDER_EXECUTIONS {
uuid id PK
uuid order_id FK "NOT NULL"
float executed_quantity "NOT NULL"
bigint executed_price "NOT NULL"
bigint executed_amount "NOT NULL"
varchar execution_id UK "NOT NULL"
timestamp executed_at
}
%% ============================================================
%% NOTIFICATION DATABASE (notification_db)
%% ============================================================
NOTIFICATION_TEMPLATES {
uuid id PK
varchar template_code UK "NOT NULL"
varchar template_name "NOT NULL"
varchar channel "PUSH|EMAIL|SMS"
varchar event_type "NOT NULL"
text subject
text template_body "NOT NULL"
jsonb variables
boolean is_active "DEFAULT TRUE"
timestamp created_at
varchar created_by
}
NOTIFICATION_LOGS {
uuid id PK
uuid user_id "NOT NULL"
varchar template_code FK "NOT NULL"
varchar channel "PUSH|EMAIL|SMS"
varchar recipient "NOT NULL"
text subject
text content "NOT NULL"
jsonb metadata
varchar status "PENDING|SENT|DELIVERED|FAILED|BOUNCED"
varchar provider "FIREBASE|SENDGRID|SMS_GATEWAY"
varchar provider_message_id
text error_message
int retry_count "DEFAULT 0"
timestamp sent_at
timestamp delivered_at
timestamp created_at
}
USER_NOTIFICATION_PREFERENCES {
uuid id PK
uuid user_id "FK, UK, NOT NULL"
boolean push_enabled "DEFAULT TRUE"
boolean email_enabled "DEFAULT TRUE"
boolean sms_enabled "DEFAULT TRUE"
jsonb channel_preferences
timestamp created_at
timestamp updated_at
}
%% ============================================================
%% RELATIONSHIPS
%% ============================================================
%% Auth Domain Relationships
USERS ||--o{ USER_IDENTITIES : "has multiple identities"
USERS ||--o{ AUTH_SESSIONS : "has sessions"
USERS ||--o{ USER_AUDIT_LOGS : "has audit logs"
USER_IDENTITIES ||--o{ OTP_LOGS : "generates OTP"
%% Ledger Domain Relationships (CORE)
LEDGER_ACCOUNTS ||--o{ LEDGER_ENTRIES : "records in"
LEDGER_TRANSACTIONS ||--o{ LEDGER_ENTRIES : "contains"
LEDGER_TRANSACTIONS ||--o| LEDGER_REVERSAL_LOG : "may be reversed"
LEDGER_TRANSACTIONS ||--o| LEDGER_REVERSAL_LOG : "reverses original"
LEDGER_ACCOUNTS ||--o{ LEDGER_ACCOUNT_SNAPSHOTS : "has daily snapshots"
%% Payment Domain Relationships
USERS ||--o{ DEPOSITS : "initiates"
USERS ||--o{ WITHDRAWALS : "initiates"
USERS ||--o{ BANK_ACCOUNTS : "owns"
WITHDRAWALS }o--|| BANK_ACCOUNTS : "withdraws to"
PSP_WEBHOOKS }o--|| DEPOSITS : "updates deposit"
PSP_WEBHOOKS }o--|| WITHDRAWALS : "updates withdrawal"
%% KYC Domain Relationships
KYC_PROFILES ||--o{ KYC_DOCUMENTS : "has documents"
KYC_PROFILES ||--o{ KYC_AUDIT_LOGS : "has audit trail"
%% Account Domain Relationships
CUSTOMERS ||--o{ ACCOUNTS : "owns accounts"
ACCOUNTS ||--o| ACCOUNT_BALANCES : "has balance"
ACCOUNTS ||--o{ ACCRUAL_LOGS : "has accrual logs"
%% Portfolio Domain Relationships
PORTFOLIOS ||--o{ PORTFOLIO_HOLDINGS : "contains holdings"
PORTFOLIOS ||--o{ ORDERS : "places orders"
PRODUCTS ||--o{ PORTFOLIO_HOLDINGS : "held in portfolios"
PRODUCTS ||--o{ ORDERS : "ordered products"
PARTNERS ||--o{ PRODUCTS : "provides products"
PARTNERS ||--o{ ORDERS : "executes orders"
ORDERS ||--o{ ORDER_EXECUTIONS : "has executions"
%% Notification Domain Relationships
NOTIFICATION_TEMPLATES ||--o{ NOTIFICATION_LOGS : "generates notifications"
%% Cross-Domain Logical Relationships (No FK in DB - Connected via user_id/account_id)
USERS ||..o| KYC_PROFILES : "has KYC profile"
USERS ||..o| CUSTOMERS : "becomes customer"
CUSTOMERS ||..o{ PORTFOLIOS : "manages portfolios"
USERS ||..o{ NOTIFICATION_LOGS : "receives notifications"
USERS ||..o| USER_NOTIFICATION_PREFERENCES : "has preferences"
ACCOUNTS ||..o{ PORTFOLIOS : "funds portfolios"
DEPOSITS }o..|| LEDGER_TRANSACTIONS : "creates ledger txn"
WITHDRAWALS }o..|| LEDGER_TRANSACTIONS : "creates ledger txn"
ACCRUAL_LOGS }o..|| LEDGER_TRANSACTIONS : "posts to ledger"
ORDERS }o..|| LEDGER_TRANSACTIONS : "creates ledger txn"
```
# Flow luồng nạp tiền
**Luồng hoạt động**:
1. User request deposit
2. Payment Service gọi PSP API
3. PSP trả về payment URL
4. User thanh toán trên bank app
5. PSP gửi webhook SUCCESS → Payment Service
6. Payment Service post ledger entry
7. Account balance được cập nhật (cache)
```mermaid
graph LR
A[👤 User] -->|1-Init| B[💰 Payment Service]
B -->|2-Create| C[(💾 deposits table)]
B -->|3-Call| D[💳 PSP]
D -->|4-User Pay| E[🏦 Bank App]
E -->|5-Confirm| D
D -->|6-Webhook| B
B -->|7-Post| F[📒 Ledger Service]
F -->|8-Write| G[(💾 ledger tables)]
B -->|9-Notify| H[📨 Notification]
H -->|10-Alert| A
style G fill:#ff6b6b,stroke:#c92a2a,stroke-width:3px,color:#fff
style F fill:#ff6b6b,stroke:#c92a2a,stroke-width:3px,color:#fff
```
**Sequence diagram**
```mermaid
sequenceDiagram
autonumber
participant User as 👤 User
participant App as 📱 App
participant Gateway as 🚪 Gateway
participant PaymentSvc as 💰 Payment<br/>Service
participant PSP as 💳 PSP
participant Bank as 🏦 Bank
participant LedgerSvc as 📒 Ledger<br/>Service
participant AccountSvc as 💼 Account<br/>Service
participant NotifSvc as 📨 Notif
participant DB as 💾 DB
Note over User,DB: PHASE 1: User Init Deposit
User->>App: Click "Nạp tiền"
App->>Gateway: POST /deposits/init<br/>{amount: 10000000}<br/>Authorization: Bearer {JWT}
Note over Gateway: JWT Verification (Local)<br/>1. Decode JWT<br/>2. Verify signature (HMAC-SHA256)<br/>3. Check expiration<br/>4. Extract user_id
Gateway->>Gateway: Verify JWT signature ✓<br/>Extract user_id from payload
Gateway->>PaymentSvc: Forward request<br/>X-User-ID: user-123
PaymentSvc->>DB: INSERT INTO deposits<br/>(status='INIT', amount=10M)
DB-->>PaymentSvc: deposit_id created
Note over PaymentSvc,PSP: PHASE 2: PSP Integration
PaymentSvc->>PSP: POST /payment/init<br/>{amount: 10M, order_id: deposit_id}
PSP-->>PaymentSvc: {psp_txn_id, payment_url}
PaymentSvc->>DB: UPDATE deposits<br/>SET status='PENDING',<br/>psp_txn_id, psp_redirect_url
PaymentSvc-->>App: {deposit_id, payment_url, status: "PENDING"}
Note over User,Bank: PHASE 3: User Payment
App->>PSP: Open payment_url (WebView)
User->>PSP: Choose bank (VCB)
PSP->>Bank: Deep link: vcbapp://payment
User->>Bank: Auth & Confirm payment
Bank->>Bank: Deduct 10M from user account
Bank->>PSP: Payment SUCCESS
PSP-->>App: Redirect: wealthhup://deposit-result
Note over PSP,DB: PHASE 4: Webhook Processing
PSP->>PaymentSvc: POST /webhooks/psp<br/>{webhook_id, event: "DEPOSIT_SUCCESS",<br/>txn_id, order_id, status: "SUCCESS"}
PaymentSvc->>PaymentSvc: Verify HMAC signature ✓
PaymentSvc->>DB: SELECT psp_webhooks<br/>WHERE webhook_id = 'WH-123'
DB-->>PaymentSvc: NOT FOUND (first time)
PaymentSvc->>DB: BEGIN TRANSACTION
PaymentSvc->>DB: INSERT INTO psp_webhooks<br/>(status='PROCESSING')
PaymentSvc->>DB: SELECT * FROM deposits<br/>WHERE id = order_id<br/>FOR UPDATE
PaymentSvc->>DB: UPDATE deposits<br/>SET status='SUCCESS'
Note over PaymentSvc,LedgerSvc: PHASE 5: Ledger Posting
PaymentSvc->>LedgerSvc: POST /internal/ledger/transactions<br/>{type: 'DEPOSIT',<br/>entries: [DR: PLATFORM_CASH, CR: USER_LIAB]}
LedgerSvc->>LedgerSvc: Validate double-entry<br/>Σ DR = Σ CR ✓
LedgerSvc->>DB: INSERT INTO ledger_transactions<br/>(status='POSTED')
LedgerSvc->>DB: INSERT INTO ledger_entries (2 rows)<br/>DR: PLATFORM_CASH +10M<br/>CR: USER_LIAB +10M
LedgerSvc-->>PaymentSvc: Transaction posted ✓
Note over PaymentSvc,NotifSvc: PHASE 6: Update Cache & Notify
PaymentSvc->>AccountSvc: POST /internal/accounts/refresh-balance
AccountSvc->>LedgerSvc: GET /internal/ledger/balance
LedgerSvc-->>AccountSvc: Balance = 10,000,000
AccountSvc->>DB: UPDATE account_balances<br/>SET ledger_balance=10M
PaymentSvc->>DB: UPDATE psp_webhooks<br/>SET status='PROCESSED'
PaymentSvc->>DB: INSERT INTO user_audit_logs<br/>(event='DEPOSIT_SUCCESS')
PaymentSvc->>DB: COMMIT TRANSACTION
PaymentSvc-->>PSP: 200 OK
PaymentSvc->>NotifSvc: Publish DepositSuccessEvent
NotifSvc-->>User: 📨 "Nạp tiền thành công 10M"
User->>App: Check balance
App->>Gateway: GET /accounts/summary
Gateway->>AccountSvc: Get balance
AccountSvc->>DB: SELECT account_balances
DB-->>AccountSvc: available_balance = 10M
AccountSvc-->>App: Balance data
App-->>User: Show: "Số dư: 10,000,000 VND" ✅
```
# Flow luồng rút tiền
**Luồng hoạt động**:
1. User request withdrawal
2. Payment Service check balance
3. Payment Service LOCK ledger entry (WITHDRAWAL_PENDING)
4. Payment Service gọi PSP payout API
5. PSP chuyển tiền đến bank của user
6. PSP gửi webhook SUCCESS → Payment Service
7. Payment Service COMPLETE ledger entry (release lock + ghi nhận fee)
8. Account balance được cập nhật (cache)
```mermaid
graph LR
A[👤 User] -->|1-Init Withdrawal| B[💰 Payment Service]
B -->|2-Check Balance| C[💼 Account Service]
C -->|3-Available?| B
B -->|4-Create| D[(💾 withdrawals table)]
B -->|5-Lock Balance| E[📒 Ledger Service]
E -->|6-Write LOCK| F[(💾 ledger tables)]
B -->|7-Payout Request| G[💳 PSP]
G -->|8-Transfer| H[🏦 User's Bank]
H -->|9-Confirm| G
G -->|10-Webhook| B
B -->|11-Complete Ledger| E
E -->|12-Write COMPLETE| F
B -->|13-Publish Event| I[📮 Kafka]
I -->|14-Fan-out| J[📨 Notification]
J -->|15-Alert| A
style E fill:#ff6b6b,stroke:#c92a2a,stroke-width:3px,color:#fff
style F fill:#ff6b6b,stroke:#c92a2a,stroke-width:3px,color:#fff
```
**Sequence diagram**:
```mermaid
sequenceDiagram
autonumber
participant User as 👤 User
participant App as 📱 App
participant GW as 🚪 Gateway
participant PaySvc as 💰 Payment
participant AccSvc as 💼 Account
participant LedgerSvc as 📒 Ledger
participant PSP as 💳 VNPT ePay
participant Bank as 🏦 Bank
participant Kafka as 🔥 Kafka
participant NotifSvc as 📨 Notification
participant InsightSvc as 📊 Insight
participant DB as 💾 DB
Note over User,DB: 📍 PHASE 1: Init Withdrawal
User->>App: 1. Click "Rút tiền"
Note over App,GW: Authentication at Gateway (Local JWT Verify)
App->>GW: 2. GET /accounts/balance<br/>Header: Authorization Bearer JWT
GW->>GW: 3. Verify JWT signature locally ✓<br/>Extract user_id from JWT payload
GW->>AccSvc: 4. Get balance (user_id)
AccSvc->>LedgerSvc: 5. Calculate balance
LedgerSvc-->>AccSvc: 6. available=10M, locked=0
AccSvc-->>GW: 7. Balance response
GW-->>App: 8. Forward response
App-->>User: 9. Display balance: 10M
User->>App: 10. Input 3,000,000 VND
User->>App: 11. Choose "Rút nhanh" (IMMEDIATE)
App-->>User: 12. Confirm: Fee 50k, Net 2.95M
User->>App: 13. Confirm
Note over App,GW: Authentication & Init Withdrawal
App->>GW: 14. POST /withdrawals/init<br/>Header: Authorization Bearer JWT<br/>Body: {amount: 3M, speed: IMMEDIATE}
GW->>GW: 15. Verify JWT signature ✓<br/>Extract user_id, account_id
GW->>GW: 16. Check rate limit (5/day) ✓
GW->>PaySvc: 17. Forward with user_id context
Note over PaySvc,LedgerSvc: 📍 PHASE 2: Validate & Lock
PaySvc->>PaySvc: 18. Calculate fee = 50k
PaySvc->>AccSvc: 19. Check available balance
AccSvc->>LedgerSvc: 20. Get balance
LedgerSvc-->>AccSvc: 21. available=10M
AccSvc-->>PaySvc: 22. Balance sufficient ✓
PaySvc->>DB: 23. BEGIN TRANSACTION
PaySvc->>DB: 24. INSERT withdrawals<br/>(status=PENDING, user_id)
Note over PaySvc,LedgerSvc: 📍 PHASE 3: Ledger Lock (CRITICAL!)
PaySvc->>LedgerSvc: 25. POST /ledger/transactions<br/>{<br/> type: WITHDRAWAL_LOCK<br/> entries: [<br/> DR: WITHDRAWAL_PENDING +3M<br/> CR: USER_FLEXIBLE_LIAB +3M<br/> ]<br/>}
LedgerSvc->>LedgerSvc: 26. Validate double-entry<br/>Σ DR = Σ CR ✓
LedgerSvc->>DB: 27. INSERT ledger_transactions<br/>INSERT ledger_entries (2 rows)
LedgerSvc-->>PaySvc: 28. Lock posted ✓
PaySvc->>DB: 29. UPDATE withdrawals<br/>SET ledger_lock_txn_id
PaySvc->>AccSvc: 30. Refresh balance
AccSvc->>DB: 31. UPDATE account_balances<br/>SET available=7M, locked=3M
PaySvc->>DB: 32. COMMIT TRANSACTION
PaySvc-->>GW: 33. Response {<br/> id, status=PENDING,<br/> estimated_arrival: "1h"<br/>}
GW-->>App: 34. Forward
App-->>User: 35. "Đang xử lý..."
Note over PaySvc,Bank: 📍 PHASE 4: PSP Processing
PaySvc->>DB: 36. SELECT bank_account<br/>WHERE id=bank_account_id
DB-->>PaySvc: 37. Bank info (encrypted)
PaySvc->>PaySvc: 38. Decrypt account_number
PaySvc->>PSP: 39. POST /payout/init<br/>{<br/> amount: 2950000 (net),<br/> bank_code: VCB,<br/> account_number: xxx,<br/> signature: hmac<br/>}
PSP->>PSP: 40. Verify signature ✓
PSP-->>PaySvc: 41. {<br/> psp_txn_id: PSP-WD-123,<br/> status: PROCESSING<br/>}
PaySvc->>DB: 42. UPDATE withdrawals<br/>SET psp_txn_id, status=PROCESSING
PSP->>Bank: 43. Transfer 2.95M to user
Bank->>Bank: 44. Process transfer
Note over PSP,NotifSvc: 📍 PHASE 5: Success Webhook
Bank-->>PSP: 45. Transfer completed ✓
PSP->>PaySvc: 46. POST /webhooks/psp<br/>{<br/> webhook_id: WH-456,<br/> event: WITHDRAWAL_SUCCESS,<br/> txn_id: PSP-WD-123,<br/> status: SUCCESS,<br/> signature: hmac<br/>}
PaySvc->>PaySvc: 47. Verify signature ✓
PaySvc->>DB: 48. BEGIN TRANSACTION
PaySvc->>DB: 49. Check idempotency<br/>webhook_id exists? NO
PaySvc->>DB: 50. INSERT psp_webhooks<br/>(status=PROCESSING)
PaySvc->>DB: 51. SELECT withdrawals FOR UPDATE
DB-->>PaySvc: 52. Withdrawal found, status=PROCESSING
PaySvc->>DB: 53. UPDATE withdrawals<br/>SET status=SUCCESS
Note over PaySvc,LedgerSvc: 📍 PHASE 6: Complete Ledger
PaySvc->>LedgerSvc: 54. POST /ledger/transactions<br/>{<br/> type: WITHDRAWAL_COMPLETE<br/> entries: [<br/> DR: PLATFORM_CASH_CLEARING 2.95M<br/> CR: WITHDRAWAL_PENDING 2.95M<br/> DR: WITHDRAWAL_PENDING 50k<br/> CR: USER_INTEREST_LIAB 50k<br/> DR: USER_INTEREST_LIAB 50k<br/> CR: PLATFORM_FEE_REVENUE 50k<br/> ]<br/>}
LedgerSvc->>LedgerSvc: 55. Validate balance<br/>Σ DR = Σ CR ✓
LedgerSvc->>DB: 56. INSERT ledger entries
LedgerSvc-->>PaySvc: 57. Complete posted ✓
PaySvc->>AccSvc: 58. Refresh balance
AccSvc->>DB: 59. UPDATE account_balances<br/>SET available=7M, locked=0
PaySvc->>DB: 60. UPDATE psp_webhooks<br/>status=PROCESSED
PaySvc->>DB: 61. COMMIT TRANSACTION
Note over PaySvc,InsightSvc: 📍 PHASE 7: Event Publishing (Kafka)
PaySvc->>Kafka: 62. Publish to "withdrawal-events"<br/>{<br/> eventType: WithdrawalSuccess,<br/> userId, withdrawalId, amount<br/>}
par Parallel Consumption
Kafka->>NotifSvc: 63. Consume (group: notification-group)
NotifSvc->>NotifSvc: 64. Process event
NotifSvc-->>User: 65. 📨 Push: "Rút tiền thành công 3M"
NotifSvc->>DB: 66. INSERT notification_logs
NotifSvc->>Kafka: 67. Commit offset ✓
and
Kafka->>InsightSvc: 68. Consume (group: insight-group)
InsightSvc->>InsightSvc: 69. Update analytics
InsightSvc->>DB: 70. INSERT analytics data
InsightSvc->>Kafka: 71. Commit offset ✓
end
PaySvc-->>PSP: 72. HTTP 200 OK
Note over User,AccSvc: User Check Balance
User->>App: 73. Check balance
App->>GW: 74. GET /accounts/balance<br/>Header: Authorization Bearer JWT
GW->>GW: 75. Verify JWT locally ✓<br/>Extract user_id
GW->>AccSvc: 76. Get balance
AccSvc-->>GW: 77. available=7M
GW-->>App: 78. Forward response
App-->>User: 79. ✅ "Số dư: 7,000,000 VND"
Note over User,DB: ✅ HOÀN TẤT: Withdrawal Success
```
# So sánh
```mermaid
graph TB
subgraph Deposit["📥 DEPOSIT FLOW"]
D1[User Init] --> D2[Create Record]
D2 --> D3[Call PSP]
D3 --> D4[User Pay on Bank]
D4 --> D5[PSP Success Webhook]
D5 --> D6[📒 Post to Ledger]
D6 --> D7[Notify User]
style D6 fill:#51cf66,stroke:#2f9e44,stroke-width:2px
end
subgraph Withdrawal["📤 WITHDRAWAL FLOW - CRITICAL DIFFERENCE"]
W1[User Init] --> W2[Check Balance]
W2 --> W3[Create Record]
W3 --> W4[📒 LOCK Ledger FIRST!]
W4 --> W5[Call PSP]
W5 --> W6[PSP Transfer to Bank]
W6 --> W7[PSP Success Webhook]
W7 --> W8[📒 COMPLETE Ledger]
W8 --> W9[Notify User]
style W4 fill:#ff6b6b,stroke:#c92a2a,stroke-width:3px,color:#fff
style W8 fill:#ff6b6b,stroke:#c92a2a,stroke-width:3px,color:#fff
end
```