feat: Khởi tạo cấu trúc dự án MerchantService mới bao gồm API, Domain, Infrastructure và các bài kiểm thử, đồng thời cập nhật các tệp liên quan trong MembershipService.
This commit is contained in:
@@ -171,14 +171,24 @@ public class Member : Entity, IAggregateRoot
|
||||
```csharp
|
||||
public class LevelDefinition : Entity, IAggregateRoot
|
||||
{
|
||||
private readonly List<LevelBenefit> _benefits = new();
|
||||
|
||||
public int LevelNumber { get; private set; } // 1, 2, 3...
|
||||
public string Name { get; private set; } // "Bronze", "Silver", "Gold"
|
||||
public int RequiredExp { get; private set; } // 0, 100, 300, 600...
|
||||
public string? Description { get; private set; }
|
||||
public string? IconUrl { get; private set; }
|
||||
public string? Benefits { get; private set; } // JSON benefits
|
||||
public string? BadgeColor { get; private set; } // "#CD7F32", "#FFD700"
|
||||
public bool IsActive { get; private set; }
|
||||
|
||||
public IReadOnlyCollection<LevelBenefit> Benefits => _benefits.AsReadOnly();
|
||||
|
||||
public void AddBenefit(LevelBenefit benefit)
|
||||
{
|
||||
_benefits.Add(benefit);
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void UpdateRequiredExp(int newRequiredExp)
|
||||
{
|
||||
if (newRequiredExp < 0)
|
||||
@@ -188,6 +198,61 @@ public class LevelDefinition : Entity, IAggregateRoot
|
||||
}
|
||||
```
|
||||
|
||||
### LevelBenefit Entity (Rewards per Level)
|
||||
|
||||
```csharp
|
||||
public class LevelBenefit : Entity
|
||||
{
|
||||
public Guid LevelDefinitionId { get; private set; }
|
||||
public string BenefitType { get; private set; } // "discount_percent", "free_shipping"
|
||||
public string BenefitValue { get; private set; } // JSON: {"percent": 10}
|
||||
public string? Description { get; private set; }
|
||||
public bool IsActive { get; private set; }
|
||||
}
|
||||
```
|
||||
|
||||
**Benefit Types:**
|
||||
|
||||
| Type | Value Example | Description |
|
||||
|------|---------------|-------------|
|
||||
| `discount_percent` | `{"percent": 10}` | 10% discount |
|
||||
| `free_shipping` | `{"enabled": true}` | Free shipping |
|
||||
| `priority_support` | `{"enabled": true}` | Priority support |
|
||||
| `bonus_points` | `{"multiplier": 1.5}` | 1.5x point multiplier |
|
||||
| `exclusive_access` | `{"feature": "early_sale"}` | Early access |
|
||||
|
||||
### ExperienceTransaction Entity (EXP Tracking)
|
||||
|
||||
Entity to track EXP source and history:
|
||||
|
||||
```csharp
|
||||
public class ExperienceTransaction : Entity
|
||||
{
|
||||
public Guid MemberId { get; private set; }
|
||||
public int Points { get; private set; }
|
||||
public ExperienceSource Source { get; private set; } // Purchase, Referral, Activity...
|
||||
public string? ReferenceId { get; private set; } // Order ID, Referral Code, etc.
|
||||
public string? Metadata { get; private set; } // JSON additional info
|
||||
public int LevelAtTime { get; private set; } // Level when EXP was earned
|
||||
public DateTime CreatedAt { get; private set; }
|
||||
}
|
||||
```
|
||||
|
||||
**Experience Sources:**
|
||||
|
||||
| Source | ID | Description |
|
||||
|--------|----|-------------|
|
||||
| `Purchase` | 1 | From orders |
|
||||
| `Referral` | 2 | Friend referral |
|
||||
| `Activity` | 3 | App activity |
|
||||
| `Promotion` | 4 | Promotions |
|
||||
| `Review` | 5 | Product reviews |
|
||||
| `CheckIn` | 6 | Daily check-in |
|
||||
| `Admin` | 7 | Admin manual grant |
|
||||
|
||||
> [!NOTE]
|
||||
> **Audit Trail**: Every time a member receives EXP, it's logged in `experience_transactions` with full source information for audit and analytics.
|
||||
|
||||
### Domain Events
|
||||
|
||||
| Event | Trigger | Purpose |
|
||||
@@ -355,6 +420,10 @@ sequenceDiagram
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
MEMBERS ||--o{ EXPERIENCE_TRANSACTIONS : has
|
||||
MEMBERS }o--|| LEVEL_DEFINITIONS : current_level
|
||||
LEVEL_DEFINITIONS ||--o{ LEVEL_BENEFITS : has
|
||||
|
||||
MEMBERS {
|
||||
uuid id PK "Same as IAM UserId"
|
||||
varchar country_code "ISO 3166-1 alpha-2"
|
||||
@@ -375,19 +444,38 @@ erDiagram
|
||||
int required_exp "EXP needed for this level"
|
||||
text description
|
||||
varchar icon_url
|
||||
jsonb benefits "Level benefits/rewards"
|
||||
varchar badge_color "Hex color code"
|
||||
boolean is_active
|
||||
timestamp created_at
|
||||
timestamp updated_at
|
||||
}
|
||||
|
||||
MEMBERS }o..|| LEVEL_DEFINITIONS : "current_level references"
|
||||
LEVEL_BENEFITS {
|
||||
uuid id PK
|
||||
uuid level_definition_id FK
|
||||
varchar benefit_type "discount_percent, free_shipping..."
|
||||
jsonb benefit_value "Benefit configuration"
|
||||
text description
|
||||
boolean is_active
|
||||
timestamp created_at
|
||||
}
|
||||
|
||||
EXPERIENCE_TRANSACTIONS {
|
||||
uuid id PK
|
||||
uuid member_id FK
|
||||
int points "EXP points earned"
|
||||
int source_id "ExperienceSource enum"
|
||||
varchar reference_id "Order ID, Referral Code..."
|
||||
jsonb metadata "Additional info"
|
||||
int level_at_time "Member level when earned"
|
||||
timestamp created_at
|
||||
}
|
||||
```
|
||||
|
||||
### SQL Schema
|
||||
|
||||
```sql
|
||||
-- Level Definitions table (Admin configurable)
|
||||
-- 1. Level Definitions table (Admin configurable)
|
||||
CREATE TABLE level_definitions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
level_number INT NOT NULL UNIQUE,
|
||||
@@ -395,13 +483,24 @@ CREATE TABLE level_definitions (
|
||||
required_exp INT NOT NULL DEFAULT 0,
|
||||
description TEXT,
|
||||
icon_url VARCHAR(500),
|
||||
benefits JSONB,
|
||||
badge_color VARCHAR(20),
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Members table
|
||||
-- 2. Level Benefits table (Rewards per level)
|
||||
CREATE TABLE level_benefits (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
level_definition_id UUID NOT NULL REFERENCES level_definitions(id),
|
||||
benefit_type VARCHAR(50) NOT NULL,
|
||||
benefit_value JSONB NOT NULL,
|
||||
description TEXT,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 3. Members table
|
||||
CREATE TABLE members (
|
||||
id UUID PRIMARY KEY,
|
||||
country_code VARCHAR(2) NOT NULL DEFAULT 'VN',
|
||||
@@ -415,20 +514,53 @@ CREATE TABLE members (
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 4. Experience Transactions table (EXP History/Tracking)
|
||||
CREATE TABLE experience_transactions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
member_id UUID NOT NULL REFERENCES members(id),
|
||||
points INT NOT NULL,
|
||||
source_id INT NOT NULL,
|
||||
reference_id VARCHAR(100),
|
||||
metadata JSONB,
|
||||
level_at_time INT NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX ix_members_current_level ON members(current_level);
|
||||
CREATE INDEX ix_members_current_exp ON members(current_exp);
|
||||
CREATE INDEX ix_level_definitions_level_number ON level_definitions(level_number);
|
||||
CREATE INDEX ix_level_definitions_is_active ON level_definitions(is_active);
|
||||
CREATE INDEX ix_level_benefits_level_definition_id ON level_benefits(level_definition_id);
|
||||
CREATE INDEX ix_members_current_level ON members(current_level);
|
||||
CREATE INDEX ix_members_current_exp ON members(current_exp);
|
||||
CREATE INDEX ix_experience_transactions_member_id ON experience_transactions(member_id);
|
||||
CREATE INDEX ix_experience_transactions_created_at ON experience_transactions(created_at);
|
||||
CREATE INDEX ix_experience_transactions_source ON experience_transactions(source_id);
|
||||
|
||||
-- Default Level Definitions
|
||||
INSERT INTO level_definitions (level_number, name, required_exp, description, is_active)
|
||||
INSERT INTO level_definitions (level_number, name, required_exp, description, badge_color, is_active)
|
||||
VALUES
|
||||
(1, 'Bronze', 0, 'Starting level', true),
|
||||
(2, 'Silver', 100, 'Reach 100 EXP', true),
|
||||
(3, 'Gold', 300, 'Reach 300 EXP', true),
|
||||
(4, 'Platinum', 600, 'Reach 600 EXP', true),
|
||||
(5, 'Diamond', 1000, 'Reach 1000 EXP', true);
|
||||
(1, 'Bronze', 0, 'Starting level - Welcome!', '#CD7F32', true),
|
||||
(2, 'Silver', 100, 'Reach 100 EXP', '#C0C0C0', true),
|
||||
(3, 'Gold', 300, 'Reach 300 EXP', '#FFD700', true),
|
||||
(4, 'Platinum', 600, 'Reach 600 EXP', '#E5E4E2', true),
|
||||
(5, 'Diamond', 1000, 'Reach 1000 EXP - Elite!', '#B9F2FF', true);
|
||||
|
||||
-- Default Level Benefits
|
||||
INSERT INTO level_benefits (level_definition_id, benefit_type, benefit_value, description)
|
||||
SELECT id, 'discount_percent', '{"percent": 5}', '5% discount on all orders'
|
||||
FROM level_definitions WHERE level_number = 2;
|
||||
|
||||
INSERT INTO level_benefits (level_definition_id, benefit_type, benefit_value, description)
|
||||
SELECT id, 'discount_percent', '{"percent": 10}', '10% discount on all orders'
|
||||
FROM level_definitions WHERE level_number = 3;
|
||||
|
||||
INSERT INTO level_benefits (level_definition_id, benefit_type, benefit_value, description)
|
||||
SELECT id, 'free_shipping', '{"enabled": true}', 'Free shipping on all orders'
|
||||
FROM level_definitions WHERE level_number = 4;
|
||||
|
||||
INSERT INTO level_benefits (level_definition_id, benefit_type, benefit_value, description)
|
||||
SELECT id, 'priority_support', '{"enabled": true}', 'Priority customer support'
|
||||
FROM level_definitions WHERE level_number = 5;
|
||||
```
|
||||
|
||||
## API Design
|
||||
@@ -438,13 +570,14 @@ VALUES
|
||||
#### Member Endpoints
|
||||
|
||||
```
|
||||
GET /api/v1/members # List (paginated, searchable)
|
||||
GET /api/v1/members/{id} # Get by ID
|
||||
GET /api/v1/members/me # Get current user
|
||||
GET /api/v1/members/{id}/progress # Get level progress
|
||||
POST /api/v1/members # Create
|
||||
POST /api/v1/members/{id}/experience # Add EXP
|
||||
PUT /api/v1/members/{id} # Update profile
|
||||
GET /api/v1/members # List (paginated, searchable)
|
||||
GET /api/v1/members/{id} # Get by ID
|
||||
GET /api/v1/members/me # Get current user
|
||||
GET /api/v1/members/{id}/progress # Get level progress
|
||||
GET /api/v1/members/{id}/experience # Get EXP history (tracking)
|
||||
POST /api/v1/members # Create
|
||||
POST /api/v1/members/{id}/experience # Add EXP
|
||||
PUT /api/v1/members/{id} # Update profile
|
||||
```
|
||||
|
||||
#### Level Endpoints (Admin)
|
||||
|
||||
@@ -171,14 +171,24 @@ public class Member : Entity, IAggregateRoot
|
||||
```csharp
|
||||
public class LevelDefinition : Entity, IAggregateRoot
|
||||
{
|
||||
private readonly List<LevelBenefit> _benefits = new();
|
||||
|
||||
public int LevelNumber { get; private set; } // 1, 2, 3...
|
||||
public string Name { get; private set; } // "Bronze", "Silver", "Gold"
|
||||
public int RequiredExp { get; private set; } // 0, 100, 300, 600...
|
||||
public string? Description { get; private set; }
|
||||
public string? IconUrl { get; private set; }
|
||||
public string? Benefits { get; private set; } // JSON benefits
|
||||
public string? BadgeColor { get; private set; } // "#CD7F32", "#FFD700"
|
||||
public bool IsActive { get; private set; }
|
||||
|
||||
public IReadOnlyCollection<LevelBenefit> Benefits => _benefits.AsReadOnly();
|
||||
|
||||
public void AddBenefit(LevelBenefit benefit)
|
||||
{
|
||||
_benefits.Add(benefit);
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void UpdateRequiredExp(int newRequiredExp)
|
||||
{
|
||||
if (newRequiredExp < 0)
|
||||
@@ -188,6 +198,61 @@ public class LevelDefinition : Entity, IAggregateRoot
|
||||
}
|
||||
```
|
||||
|
||||
### LevelBenefit Entity (Rewards cho mỗi Level)
|
||||
|
||||
```csharp
|
||||
public class LevelBenefit : Entity
|
||||
{
|
||||
public Guid LevelDefinitionId { get; private set; }
|
||||
public string BenefitType { get; private set; } // "discount_percent", "free_shipping"
|
||||
public string BenefitValue { get; private set; } // JSON: {"percent": 10}
|
||||
public string? Description { get; private set; }
|
||||
public bool IsActive { get; private set; }
|
||||
}
|
||||
```
|
||||
|
||||
**Benefit Types:**
|
||||
|
||||
| Type | Value Example | Description |
|
||||
|------|---------------|-------------|
|
||||
| `discount_percent` | `{"percent": 10}` | Giảm giá 10% |
|
||||
| `free_shipping` | `{"enabled": true}` | Miễn phí ship |
|
||||
| `priority_support` | `{"enabled": true}` | Hỗ trợ ưu tiên |
|
||||
| `bonus_points` | `{"multiplier": 1.5}` | Nhân điểm 1.5x |
|
||||
| `exclusive_access` | `{"feature": "early_sale"}` | Truy cập sớm |
|
||||
|
||||
### ExperienceTransaction Entity (EXP Tracking)
|
||||
|
||||
Entity để tracking nguồn gốc và lịch sử EXP:
|
||||
|
||||
```csharp
|
||||
public class ExperienceTransaction : Entity
|
||||
{
|
||||
public Guid MemberId { get; private set; }
|
||||
public int Points { get; private set; }
|
||||
public ExperienceSource Source { get; private set; } // Purchase, Referral, Activity...
|
||||
public string? ReferenceId { get; private set; } // Order ID, Referral Code, etc.
|
||||
public string? Metadata { get; private set; } // JSON additional info
|
||||
public int LevelAtTime { get; private set; } // Level khi nhận EXP
|
||||
public DateTime CreatedAt { get; private set; }
|
||||
}
|
||||
```
|
||||
|
||||
**Experience Sources:**
|
||||
|
||||
| Source | ID | Mô tả |
|
||||
|--------|----|-------|
|
||||
| `Purchase` | 1 | Từ đơn hàng |
|
||||
| `Referral` | 2 | Giới thiệu bạn bè |
|
||||
| `Activity` | 3 | Hoạt động trên app |
|
||||
| `Promotion` | 4 | Khuyến mãi |
|
||||
| `Review` | 5 | Đánh giá sản phẩm |
|
||||
| `CheckIn` | 6 | Check-in hàng ngày |
|
||||
| `Admin` | 7 | Admin manual grant |
|
||||
|
||||
> [!NOTE]
|
||||
> **Audit Trail**: Mỗi lần member nhận EXP đều được lưu vào `experience_transactions` với đầy đủ thông tin nguồn gốc để audit và analytics.
|
||||
|
||||
### Domain Events
|
||||
|
||||
| Event | Trigger | Mục đích |
|
||||
@@ -355,6 +420,10 @@ sequenceDiagram
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
MEMBERS ||--o{ EXPERIENCE_TRANSACTIONS : has
|
||||
MEMBERS }o--|| LEVEL_DEFINITIONS : current_level
|
||||
LEVEL_DEFINITIONS ||--o{ LEVEL_BENEFITS : has
|
||||
|
||||
MEMBERS {
|
||||
uuid id PK "Same as IAM UserId"
|
||||
varchar country_code "ISO 3166-1 alpha-2"
|
||||
@@ -375,19 +444,38 @@ erDiagram
|
||||
int required_exp "EXP needed for this level"
|
||||
text description
|
||||
varchar icon_url
|
||||
jsonb benefits "Level benefits/rewards"
|
||||
varchar badge_color "Hex color code"
|
||||
boolean is_active
|
||||
timestamp created_at
|
||||
timestamp updated_at
|
||||
}
|
||||
|
||||
MEMBERS }o..|| LEVEL_DEFINITIONS : "current_level references"
|
||||
LEVEL_BENEFITS {
|
||||
uuid id PK
|
||||
uuid level_definition_id FK
|
||||
varchar benefit_type "discount_percent, free_shipping..."
|
||||
jsonb benefit_value "Benefit configuration"
|
||||
text description
|
||||
boolean is_active
|
||||
timestamp created_at
|
||||
}
|
||||
|
||||
EXPERIENCE_TRANSACTIONS {
|
||||
uuid id PK
|
||||
uuid member_id FK
|
||||
int points "EXP points earned"
|
||||
int source_id "ExperienceSource enum"
|
||||
varchar reference_id "Order ID, Referral Code..."
|
||||
jsonb metadata "Additional info"
|
||||
int level_at_time "Member level when earned"
|
||||
timestamp created_at
|
||||
}
|
||||
```
|
||||
|
||||
### SQL Schema
|
||||
|
||||
```sql
|
||||
-- Bảng Level Definitions (Admin configurable)
|
||||
-- 1. Bảng Level Definitions (Admin configurable)
|
||||
CREATE TABLE level_definitions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
level_number INT NOT NULL UNIQUE,
|
||||
@@ -395,13 +483,24 @@ CREATE TABLE level_definitions (
|
||||
required_exp INT NOT NULL DEFAULT 0,
|
||||
description TEXT,
|
||||
icon_url VARCHAR(500),
|
||||
benefits JSONB,
|
||||
badge_color VARCHAR(20),
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Bảng Members
|
||||
-- 2. Bảng Level Benefits (Rewards cho mỗi level)
|
||||
CREATE TABLE level_benefits (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
level_definition_id UUID NOT NULL REFERENCES level_definitions(id),
|
||||
benefit_type VARCHAR(50) NOT NULL,
|
||||
benefit_value JSONB NOT NULL,
|
||||
description TEXT,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 3. Bảng Members
|
||||
CREATE TABLE members (
|
||||
id UUID PRIMARY KEY,
|
||||
country_code VARCHAR(2) NOT NULL DEFAULT 'VN',
|
||||
@@ -415,20 +514,53 @@ CREATE TABLE members (
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 4. Bảng Experience Transactions (EXP History/Tracking)
|
||||
CREATE TABLE experience_transactions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
member_id UUID NOT NULL REFERENCES members(id),
|
||||
points INT NOT NULL,
|
||||
source_id INT NOT NULL,
|
||||
reference_id VARCHAR(100),
|
||||
metadata JSONB,
|
||||
level_at_time INT NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX ix_members_current_level ON members(current_level);
|
||||
CREATE INDEX ix_members_current_exp ON members(current_exp);
|
||||
CREATE INDEX ix_level_definitions_level_number ON level_definitions(level_number);
|
||||
CREATE INDEX ix_level_definitions_is_active ON level_definitions(is_active);
|
||||
CREATE INDEX ix_level_benefits_level_definition_id ON level_benefits(level_definition_id);
|
||||
CREATE INDEX ix_members_current_level ON members(current_level);
|
||||
CREATE INDEX ix_members_current_exp ON members(current_exp);
|
||||
CREATE INDEX ix_experience_transactions_member_id ON experience_transactions(member_id);
|
||||
CREATE INDEX ix_experience_transactions_created_at ON experience_transactions(created_at);
|
||||
CREATE INDEX ix_experience_transactions_source ON experience_transactions(source_id);
|
||||
|
||||
-- Default Level Definitions
|
||||
INSERT INTO level_definitions (level_number, name, required_exp, description, is_active)
|
||||
INSERT INTO level_definitions (level_number, name, required_exp, description, badge_color, is_active)
|
||||
VALUES
|
||||
(1, 'Bronze', 0, 'Starting level', true),
|
||||
(2, 'Silver', 100, 'Reach 100 EXP', true),
|
||||
(3, 'Gold', 300, 'Reach 300 EXP', true),
|
||||
(4, 'Platinum', 600, 'Reach 600 EXP', true),
|
||||
(5, 'Diamond', 1000, 'Reach 1000 EXP', true);
|
||||
(1, 'Bronze', 0, 'Starting level - Welcome!', '#CD7F32', true),
|
||||
(2, 'Silver', 100, 'Reach 100 EXP', '#C0C0C0', true),
|
||||
(3, 'Gold', 300, 'Reach 300 EXP', '#FFD700', true),
|
||||
(4, 'Platinum', 600, 'Reach 600 EXP', '#E5E4E2', true),
|
||||
(5, 'Diamond', 1000, 'Reach 1000 EXP - Elite!', '#B9F2FF', true);
|
||||
|
||||
-- Default Level Benefits
|
||||
INSERT INTO level_benefits (level_definition_id, benefit_type, benefit_value, description)
|
||||
SELECT id, 'discount_percent', '{"percent": 5}', '5% discount on all orders'
|
||||
FROM level_definitions WHERE level_number = 2;
|
||||
|
||||
INSERT INTO level_benefits (level_definition_id, benefit_type, benefit_value, description)
|
||||
SELECT id, 'discount_percent', '{"percent": 10}', '10% discount on all orders'
|
||||
FROM level_definitions WHERE level_number = 3;
|
||||
|
||||
INSERT INTO level_benefits (level_definition_id, benefit_type, benefit_value, description)
|
||||
SELECT id, 'free_shipping', '{"enabled": true}', 'Free shipping on all orders'
|
||||
FROM level_definitions WHERE level_number = 4;
|
||||
|
||||
INSERT INTO level_benefits (level_definition_id, benefit_type, benefit_value, description)
|
||||
SELECT id, 'priority_support', '{"enabled": true}', 'Priority customer support'
|
||||
FROM level_definitions WHERE level_number = 5;
|
||||
```
|
||||
|
||||
## Thiết Kế API
|
||||
@@ -438,13 +570,14 @@ VALUES
|
||||
#### Member Endpoints
|
||||
|
||||
```
|
||||
GET /api/v1/members # Danh sách (phân trang, search)
|
||||
GET /api/v1/members/{id} # Lấy theo ID
|
||||
GET /api/v1/members/me # Lấy user hiện tại
|
||||
GET /api/v1/members/{id}/progress # Lấy tiến trình level
|
||||
POST /api/v1/members # Tạo mới
|
||||
POST /api/v1/members/{id}/experience # Thêm EXP
|
||||
PUT /api/v1/members/{id} # Cập nhật hồ sơ
|
||||
GET /api/v1/members # Danh sách (phân trang, search)
|
||||
GET /api/v1/members/{id} # Lấy theo ID
|
||||
GET /api/v1/members/me # Lấy user hiện tại
|
||||
GET /api/v1/members/{id}/progress # Lấy tiến trình level
|
||||
GET /api/v1/members/{id}/experience # Lấy lịch sử EXP (tracking)
|
||||
POST /api/v1/members # Tạo mới
|
||||
POST /api/v1/members/{id}/experience # Thêm EXP
|
||||
PUT /api/v1/members/{id} # Cập nhật hồ sơ
|
||||
```
|
||||
|
||||
#### Level Endpoints (Admin)
|
||||
|
||||
106
services/merchant-service-net/README.md
Normal file
106
services/merchant-service-net/README.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# Merchant Service .NET 10
|
||||
|
||||
> **Merchant & Shop Management Service for GoodGo Platform**
|
||||
|
||||
[](https://dotnet.microsoft.com/)
|
||||
[](https://www.postgresql.org/)
|
||||
[](https://www.docker.com/)
|
||||
|
||||
## Overview
|
||||
|
||||
Merchant Service manages Shop Owners (Merchants), Shops, and Staff for the GoodGo ecosystem. It supports online stores, physical stores, and hybrid business models.
|
||||
|
||||
### Key Features
|
||||
|
||||
- 🏪 **Shop Management** - Create and manage shops (Online/Physical/Hybrid)
|
||||
- 👥 **Staff Management** - Employee management with role-based permissions
|
||||
- 💳 **POS Support** - PIN authentication for Point-of-Sale systems
|
||||
- 📍 **Branch Management** - Multi-location support with geo-search
|
||||
- 🔐 **Role-based Access** - Merchant, MerchantStaff, MerchantAdmin roles
|
||||
- 🔗 **Service Integration** - IAM, Wallet, Membership, Chat services
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Clone and navigate
|
||||
cd services/merchant-service-net
|
||||
|
||||
# Restore and build
|
||||
dotnet restore
|
||||
dotnet build
|
||||
|
||||
# Run
|
||||
dotnet run --project src/MerchantService.API
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
| Language | README | Architecture |
|
||||
|----------|--------|--------------|
|
||||
| 🇻🇳 Tiếng Việt | [docs/vi/README.md](docs/vi/README.md) | [docs/vi/ARCHITECTURE.md](docs/vi/ARCHITECTURE.md) |
|
||||
| 🇬🇧 English | [docs/en/README.md](docs/en/README.md) | [docs/en/ARCHITECTURE.md](docs/en/ARCHITECTURE.md) |
|
||||
|
||||
## API Endpoints
|
||||
|
||||
| Resource | Base Path | Description |
|
||||
|----------|-----------|-------------|
|
||||
| Merchants | `/api/v1/merchants` | Merchant registration & management |
|
||||
| Shops | `/api/v1/shops` | Shop CRUD operations |
|
||||
| Branches | `/api/v1/shops/{id}/branches` | Physical shop locations |
|
||||
| Staff | `/api/v1/merchants/me/staff` | Staff management |
|
||||
| POS | `/api/v1/pos` | POS device authentication |
|
||||
| Admin | `/api/v1/admin/merchants` | Administrative operations |
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
merchant-service-net/
|
||||
├── src/
|
||||
│ ├── MerchantService.API/ # Controllers, Commands, Queries
|
||||
│ ├── MerchantService.Domain/ # Entities, Value Objects, Events
|
||||
│ └── MerchantService.Infrastructure/# DbContext, Repositories
|
||||
├── tests/
|
||||
│ ├── MerchantService.UnitTests/
|
||||
│ └── MerchantService.FunctionalTests/
|
||||
├── docs/
|
||||
│ ├── en/ # English documentation
|
||||
│ └── vi/ # Vietnamese documentation
|
||||
├── Dockerfile
|
||||
└── MerchantService.slnx
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description | Required |
|
||||
|----------|-------------|----------|
|
||||
| `DATABASE_URL` | PostgreSQL connection string | ✅ |
|
||||
| `Jwt__Authority` | IAM Service URL | ✅ |
|
||||
| `IamService__BaseUrl` | IAM Service base URL | ✅ |
|
||||
| `WalletService__BaseUrl` | Wallet Service base URL | ❌ |
|
||||
|
||||
## Docker
|
||||
|
||||
```bash
|
||||
# Build
|
||||
docker build -t goodgo/merchant-service:latest .
|
||||
|
||||
# Run
|
||||
docker run -p 5003:8080 \
|
||||
-e DATABASE_URL="Host=db;Port=5432;Database=merchant;..." \
|
||||
-e Jwt__Authority="http://iam-service:8080" \
|
||||
goodgo/merchant-service:latest
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Unit tests
|
||||
dotnet test tests/MerchantService.UnitTests
|
||||
|
||||
# Integration tests
|
||||
dotnet test tests/MerchantService.FunctionalTests
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This project is part of the GoodGo Platform.
|
||||
788
services/merchant-service-net/docs/en/ARCHITECTURE.md
Normal file
788
services/merchant-service-net/docs/en/ARCHITECTURE.md
Normal file
@@ -0,0 +1,788 @@
|
||||
# Merchant Service Architecture
|
||||
|
||||
## System Overview
|
||||
|
||||
Merchant Service manages **Merchants (Shop Owners)**, **Shops**, and **Merchant Staff** for the GoodGo ecosystem. This service supports multiple business types: online stores, physical stores, and hybrid models.
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph Client["Client Layer"]
|
||||
WEB["🌐 Merchant Portal"]
|
||||
POS["💳 POS System"]
|
||||
MOBILE["📱 Customer App"]
|
||||
end
|
||||
|
||||
subgraph Gateway["API Gateway"]
|
||||
TRAEFIK["⚡ Traefik"]
|
||||
end
|
||||
|
||||
subgraph Services["Microservices"]
|
||||
IAM["🔐 IAM Service<br/>(User & Roles)"]
|
||||
MERCHANT["🏪 Merchant Service<br/>(Shop, Staff)"]
|
||||
WALLET["💰 Wallet Service"]
|
||||
MEMBER["👥 Membership Service"]
|
||||
VOUCHER["🎁 Voucher Service"]
|
||||
end
|
||||
|
||||
subgraph Data["Data Layer"]
|
||||
PG[("🐘 PostgreSQL")]
|
||||
end
|
||||
|
||||
WEB --> TRAEFIK
|
||||
POS --> TRAEFIK
|
||||
MOBILE --> TRAEFIK
|
||||
TRAEFIK --> |"/api/v1/auth"| IAM
|
||||
TRAEFIK --> |"/api/v1/merchants<br/>/api/v1/shops"| MERCHANT
|
||||
TRAEFIK --> |"/api/v1/wallets"| WALLET
|
||||
|
||||
IAM --> |"JWT Token"| MERCHANT
|
||||
MERCHANT --> |"Assign Roles"| IAM
|
||||
MERCHANT --> |"Settlement"| WALLET
|
||||
MERCHANT -.-> |"Customer Visit"| MEMBER
|
||||
MERCHANT -.-> |"Voucher Redemption"| VOUCHER
|
||||
MERCHANT --> PG
|
||||
|
||||
style IAM fill:#3b82f6,stroke:#1e40af,color:#fff
|
||||
style MERCHANT fill:#f97316,stroke:#c2410c,color:#fff
|
||||
style WALLET fill:#a855f7,stroke:#7e22ce,color:#fff
|
||||
style MEMBER fill:#22c55e,stroke:#15803d,color:#fff
|
||||
style VOUCHER fill:#ec4899,stroke:#be185d,color:#fff
|
||||
style TRAEFIK fill:#6366f1,stroke:#4338ca,color:#fff
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Responsibility Separation:**
|
||||
> - **IAM Service**: Manages User identity, Roles (Merchant, MerchantStaff)
|
||||
> - **Merchant Service**: Manages Merchant, Shop, ShopBranch, MerchantStaff
|
||||
> - **Wallet Service**: Settlement, Commission calculation
|
||||
> - **Voucher Service**: Gift Voucher (planned)
|
||||
|
||||
## Clean Architecture Layers
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ API Layer (Presentation) │
|
||||
│ Controllers, Commands, Queries, Validators, DTOs │
|
||||
├────────────────────────────────────────────────────────────┤
|
||||
│ Domain Layer (Core) │
|
||||
│ Entities, Aggregates, Value Objects, Domain Events │
|
||||
│ Merchant, Shop, MerchantStaff Aggregates │
|
||||
├────────────────────────────────────────────────────────────┤
|
||||
│ Infrastructure Layer │
|
||||
│ DbContext, Repositories, Entity Configurations │
|
||||
│ External Services (IAM Client, Wallet Client) │
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Domain Model
|
||||
|
||||
### Aggregates Overview
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class Merchant {
|
||||
+Guid Id
|
||||
+Guid UserId
|
||||
+Guid? OrganizationId
|
||||
+string BusinessName
|
||||
+MerchantType Type
|
||||
+MerchantStatus Status
|
||||
+BusinessInfo BusinessInfo
|
||||
+SettlementConfig SettlementConfig
|
||||
+Register()
|
||||
+Approve()
|
||||
+Suspend()
|
||||
+CreateShop()
|
||||
}
|
||||
|
||||
class Shop {
|
||||
+Guid Id
|
||||
+Guid MerchantId
|
||||
+string Name
|
||||
+string Slug
|
||||
+ShopType Type
|
||||
+BusinessCategory Category
|
||||
+ShopStatus Status
|
||||
+ContactInfo ContactInfo
|
||||
+OperatingHours? Hours
|
||||
+AddBranch()
|
||||
+AssignStaff()
|
||||
+UpdateSettings()
|
||||
}
|
||||
|
||||
class ShopBranch {
|
||||
+Guid Id
|
||||
+Guid ShopId
|
||||
+string Name
|
||||
+string Code
|
||||
+Address Address
|
||||
+GeoLocation? Location
|
||||
+bool IsActive
|
||||
}
|
||||
|
||||
class MerchantStaff {
|
||||
+Guid Id
|
||||
+Guid UserId
|
||||
+Guid MerchantId
|
||||
+StaffRole Role
|
||||
+StaffStatus Status
|
||||
+string? EmployeeCode
|
||||
+StaffPermissions Permissions
|
||||
+SetPinCode()
|
||||
+RegisterDevice()
|
||||
}
|
||||
|
||||
class ShopMember {
|
||||
+Guid StaffId
|
||||
+Guid ShopId
|
||||
+Guid? BranchId
|
||||
+ShopRole Role
|
||||
+int? CustomPermissions
|
||||
}
|
||||
|
||||
Merchant "1" --> "*" Shop : owns
|
||||
Merchant "1" --> "*" MerchantStaff : employs
|
||||
Shop "1" --> "*" ShopBranch : has
|
||||
Shop "1" --> "*" ShopMember : assigns
|
||||
MerchantStaff "1" --> "*" ShopMember : works at
|
||||
|
||||
style Merchant fill:#f97316,stroke:#c2410c,color:#fff
|
||||
style Shop fill:#3b82f6,stroke:#1e40af,color:#fff
|
||||
style MerchantStaff fill:#22c55e,stroke:#15803d,color:#fff
|
||||
```
|
||||
|
||||
### Merchant Aggregate (Aggregate Root)
|
||||
|
||||
```csharp
|
||||
public class Merchant : Entity, IAggregateRoot
|
||||
{
|
||||
private Guid _userId; // Reference to IAM User (Owner)
|
||||
private Guid? _organizationId; // Reference to IAM Organization
|
||||
private string _businessName;
|
||||
private MerchantType _type; // Individual, Company
|
||||
private MerchantStatus _status; // Pending, Active, Suspended, Banned
|
||||
private MerchantVerification _verification;
|
||||
private SettlementConfig _settlementConfig;
|
||||
|
||||
// Value Object for business details
|
||||
public BusinessInfo BusinessInfo { get; private set; }
|
||||
|
||||
// Collections
|
||||
private readonly List<Shop> _shops = new();
|
||||
private readonly List<MerchantStaff> _staff = new();
|
||||
|
||||
public IReadOnlyCollection<Shop> Shops => _shops.AsReadOnly();
|
||||
public IReadOnlyCollection<MerchantStaff> Staff => _staff.AsReadOnly();
|
||||
|
||||
/// <summary>
|
||||
/// Factory method to register a new merchant.
|
||||
/// </summary>
|
||||
public static Merchant Register(Guid userId, string businessName, MerchantType type)
|
||||
{
|
||||
var merchant = new Merchant
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
_userId = userId,
|
||||
_businessName = businessName,
|
||||
_type = type,
|
||||
_status = MerchantStatus.PendingApproval,
|
||||
_verification = new MerchantVerification()
|
||||
};
|
||||
|
||||
merchant.AddDomainEvent(new MerchantRegisteredDomainEvent(merchant));
|
||||
return merchant;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Approve merchant application.
|
||||
/// </summary>
|
||||
public void Approve(Guid approvedBy)
|
||||
{
|
||||
if (_status != MerchantStatus.PendingApproval)
|
||||
throw new DomainException("Can only approve pending merchants");
|
||||
|
||||
_status = MerchantStatus.Active;
|
||||
AddDomainEvent(new MerchantApprovedDomainEvent(this, approvedBy));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new shop under this merchant.
|
||||
/// </summary>
|
||||
public Shop CreateShop(string name, string slug, ShopType type, BusinessCategory category)
|
||||
{
|
||||
if (_status != MerchantStatus.Active)
|
||||
throw new DomainException("Only active merchants can create shops");
|
||||
|
||||
var shop = new Shop(Id, name, slug, type, category);
|
||||
_shops.Add(shop);
|
||||
return shop;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Shop Aggregate (Aggregate Root)
|
||||
|
||||
```csharp
|
||||
public class Shop : Entity, IAggregateRoot
|
||||
{
|
||||
private Guid _merchantId;
|
||||
private string _name;
|
||||
private string _slug;
|
||||
private ShopType _type;
|
||||
private BusinessCategory _category;
|
||||
private ShopStatus _status;
|
||||
private ContactInfo _contactInfo;
|
||||
private OperatingHours? _operatingHours;
|
||||
private ShopSettings _settings;
|
||||
|
||||
// Physical locations
|
||||
private readonly List<ShopBranch> _branches = new();
|
||||
|
||||
// Staff assignments
|
||||
private readonly List<ShopMember> _members = new();
|
||||
|
||||
public IReadOnlyCollection<ShopBranch> Branches => _branches.AsReadOnly();
|
||||
public IReadOnlyCollection<ShopMember> Members => _members.AsReadOnly();
|
||||
|
||||
/// <summary>
|
||||
/// Add a physical branch to the shop.
|
||||
/// </summary>
|
||||
public ShopBranch AddBranch(string name, Address address, GeoLocation? location = null)
|
||||
{
|
||||
if (_type == ShopType.OnlineOnly)
|
||||
throw new DomainException("Online-only shops cannot have physical branches");
|
||||
|
||||
var branch = new ShopBranch(Id, name, address, location);
|
||||
_branches.Add(branch);
|
||||
AddDomainEvent(new ShopBranchAddedDomainEvent(this, branch));
|
||||
return branch;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Assign a staff member to this shop.
|
||||
/// </summary>
|
||||
public void AssignStaff(MerchantStaff staff, ShopRole role, Guid? branchId = null)
|
||||
{
|
||||
var member = new ShopMember(staff.Id, Id, role, branchId);
|
||||
_members.Add(member);
|
||||
AddDomainEvent(new StaffAssignedToShopDomainEvent(staff, this, role));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### MerchantStaff Aggregate (Aggregate Root)
|
||||
|
||||
```csharp
|
||||
public class MerchantStaff : Entity, IAggregateRoot
|
||||
{
|
||||
private Guid _userId; // Reference to IAM User
|
||||
private Guid _merchantId; // Employer
|
||||
private StaffRole _role; // Cashier, Manager, Admin
|
||||
private StaffStatus _status;
|
||||
private string? _employeeCode;
|
||||
private ContactInfo _contactInfo;
|
||||
|
||||
// For POS System quick authentication
|
||||
private readonly List<DeviceToken> _deviceTokens = new();
|
||||
private string? _pinCodeHash; // Hashed 4-6 digit PIN
|
||||
|
||||
// Permissions bitmask
|
||||
private StaffPermissions _permissions;
|
||||
|
||||
/// <summary>
|
||||
/// Set PIN code for POS authentication.
|
||||
/// </summary>
|
||||
public void SetPinCode(string pin)
|
||||
{
|
||||
if (pin.Length < 4 || pin.Length > 6 || !pin.All(char.IsDigit))
|
||||
throw new DomainException("PIN must be 4-6 digits");
|
||||
|
||||
_pinCodeHash = HashPin(pin);
|
||||
AddDomainEvent(new StaffPinCodeSetDomainEvent(this));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify PIN code for POS login.
|
||||
/// </summary>
|
||||
public bool VerifyPinCode(string pin)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_pinCodeHash))
|
||||
return false;
|
||||
|
||||
return VerifyHash(pin, _pinCodeHash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register a device for push notifications.
|
||||
/// </summary>
|
||||
public void RegisterDevice(string deviceId, string deviceName, string fcmToken)
|
||||
{
|
||||
var existingToken = _deviceTokens.FirstOrDefault(t => t.DeviceId == deviceId);
|
||||
if (existingToken != null)
|
||||
{
|
||||
existingToken.Update(fcmToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
var token = new DeviceToken(deviceId, deviceName, fcmToken);
|
||||
_deviceTokens.Add(token);
|
||||
}
|
||||
|
||||
AddDomainEvent(new StaffDeviceRegisteredDomainEvent(this, deviceId));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Value Objects
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// Business information for merchant verification.
|
||||
/// </summary>
|
||||
public record BusinessInfo(
|
||||
string? TaxId,
|
||||
string? BusinessLicenseNumber,
|
||||
string? CompanyRegistrationNumber,
|
||||
DateTime? EstablishedDate
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Settlement configuration for merchant payouts.
|
||||
/// </summary>
|
||||
public record SettlementConfig(
|
||||
BankAccount BankAccount,
|
||||
decimal CommissionRate, // Platform fee (%)
|
||||
SettlementCycle Cycle, // Daily, Weekly, Monthly
|
||||
bool AutoSettlement
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Bank account for settlement.
|
||||
/// </summary>
|
||||
public record BankAccount(
|
||||
string BankCode,
|
||||
string BankName,
|
||||
string AccountNumber,
|
||||
string AccountHolderName
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Physical address.
|
||||
/// </summary>
|
||||
public record Address(
|
||||
string Street,
|
||||
string Ward,
|
||||
string District,
|
||||
string City,
|
||||
string Province,
|
||||
string PostalCode,
|
||||
string CountryCode = "VN"
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Geographic location for map display and nearby search.
|
||||
/// </summary>
|
||||
public record GeoLocation(double Latitude, double Longitude);
|
||||
|
||||
/// <summary>
|
||||
/// Contact information.
|
||||
/// </summary>
|
||||
public record ContactInfo(
|
||||
string Phone,
|
||||
string? Email,
|
||||
string? Website
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Operating hours for physical shops.
|
||||
/// </summary>
|
||||
public record OperatingHours(
|
||||
TimeOnly OpenTime,
|
||||
TimeOnly CloseTime,
|
||||
List<DayOfWeek> OpenDays
|
||||
);
|
||||
```
|
||||
|
||||
### Enumerations
|
||||
|
||||
```csharp
|
||||
// Merchant Types
|
||||
public class MerchantType : Enumeration
|
||||
{
|
||||
public static readonly MerchantType Individual = new(1, "Individual");
|
||||
public static readonly MerchantType Company = new(2, "Company");
|
||||
}
|
||||
|
||||
// Merchant Status
|
||||
public class MerchantStatus : Enumeration
|
||||
{
|
||||
public static readonly MerchantStatus PendingApproval = new(1, "PendingApproval");
|
||||
public static readonly MerchantStatus Active = new(2, "Active");
|
||||
public static readonly MerchantStatus Suspended = new(3, "Suspended");
|
||||
public static readonly MerchantStatus Banned = new(4, "Banned");
|
||||
}
|
||||
|
||||
// Shop Types
|
||||
public class ShopType : Enumeration
|
||||
{
|
||||
public static readonly ShopType OnlineOnly = new(1, "OnlineOnly");
|
||||
public static readonly ShopType PhysicalOnly = new(2, "PhysicalOnly");
|
||||
public static readonly ShopType Hybrid = new(3, "Hybrid");
|
||||
}
|
||||
|
||||
// Business Categories
|
||||
public class BusinessCategory : Enumeration
|
||||
{
|
||||
public static readonly BusinessCategory FoodBeverage = new(1, "FoodBeverage");
|
||||
public static readonly BusinessCategory Fashion = new(2, "Fashion");
|
||||
public static readonly BusinessCategory Electronics = new(3, "Electronics");
|
||||
public static readonly BusinessCategory Healthcare = new(4, "Healthcare");
|
||||
public static readonly BusinessCategory Beauty = new(5, "Beauty");
|
||||
public static readonly BusinessCategory Education = new(6, "Education");
|
||||
public static readonly BusinessCategory Entertainment = new(7, "Entertainment");
|
||||
public static readonly BusinessCategory Services = new(8, "Services");
|
||||
public static readonly BusinessCategory Other = new(99, "Other");
|
||||
}
|
||||
|
||||
// Shop Roles (for Staff assignment at shop level)
|
||||
public class ShopRole : Enumeration
|
||||
{
|
||||
public static readonly ShopRole Cashier = new(1, "Cashier");
|
||||
public static readonly ShopRole Waiter = new(2, "Waiter");
|
||||
public static readonly ShopRole Manager = new(3, "Manager");
|
||||
public static readonly ShopRole Owner = new(4, "Owner");
|
||||
}
|
||||
|
||||
// Staff Permissions (Bitmask)
|
||||
[Flags]
|
||||
public enum StaffPermissions
|
||||
{
|
||||
None = 0,
|
||||
ViewSales = 1,
|
||||
ProcessPayment = 2,
|
||||
RefundOrder = 4,
|
||||
ManageInventory = 8,
|
||||
ViewReports = 16,
|
||||
ManageStaff = 32,
|
||||
ManageSettings = 64,
|
||||
All = int.MaxValue
|
||||
}
|
||||
```
|
||||
|
||||
### Domain Events
|
||||
|
||||
| Event | Trigger | Purpose |
|
||||
|-------|---------|---------|
|
||||
| `MerchantRegisteredDomainEvent` | New merchant registration | Notify admin for review |
|
||||
| `MerchantApprovedDomainEvent` | Admin approval | **Assign "Merchant" role via IAM** |
|
||||
| `MerchantSuspendedDomainEvent` | Merchant suspended | Notify merchant, disable shops |
|
||||
| `ShopCreatedDomainEvent` | New shop created | Analytics, indexing |
|
||||
| `ShopBranchAddedDomainEvent` | Branch added | Geo-indexing for nearby search |
|
||||
| `StaffInvitedDomainEvent` | Staff invitation | Send invitation email |
|
||||
| `StaffJoinedDomainEvent` | Staff accepted invite | **Assign "MerchantStaff" role via IAM** |
|
||||
| `StaffDeviceRegisteredDomainEvent` | POS device registered | Enable push notifications |
|
||||
|
||||
## CQRS Pattern
|
||||
|
||||
### Write Side (Commands)
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Commands["✏️ Commands"]
|
||||
CMD1["RegisterMerchantCommand"]
|
||||
CMD2["CreateShopCommand"]
|
||||
CMD3["AddShopBranchCommand"]
|
||||
CMD4["InviteStaffCommand"]
|
||||
CMD5["SetStaffPinCommand"]
|
||||
end
|
||||
|
||||
subgraph Handlers["⚙️ Command Handlers"]
|
||||
H1["RegisterMerchantHandler"]
|
||||
H2["CreateShopHandler"]
|
||||
H3["AddShopBranchHandler"]
|
||||
H4["InviteStaffHandler"]
|
||||
H5["SetStaffPinHandler"]
|
||||
end
|
||||
|
||||
subgraph Domain["🏛️ Domain Model"]
|
||||
DM["EF Core<br/>Repository"]
|
||||
end
|
||||
|
||||
DB[("🐘 Database")]
|
||||
|
||||
CMD1 --> H1 --> DM --> DB
|
||||
CMD2 --> H2 --> DM
|
||||
CMD3 --> H3 --> DM
|
||||
CMD4 --> H4 --> DM
|
||||
CMD5 --> H5 --> DM
|
||||
|
||||
style CMD1 fill:#ef4444,stroke:#b91c1c,color:#fff
|
||||
style CMD2 fill:#ef4444,stroke:#b91c1c,color:#fff
|
||||
style CMD3 fill:#ef4444,stroke:#b91c1c,color:#fff
|
||||
```
|
||||
|
||||
### Read Side (Queries)
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Queries["📖 Queries"]
|
||||
Q1["GetMerchantByIdQuery"]
|
||||
Q2["GetShopsByMerchantQuery"]
|
||||
Q3["GetNearbyShopsQuery"]
|
||||
Q4["GetStaffByShopQuery"]
|
||||
end
|
||||
|
||||
subgraph Handlers["🔍 Query Handlers"]
|
||||
QH1["GetMerchantByIdHandler"]
|
||||
QH2["GetShopsByMerchantHandler"]
|
||||
QH3["GetNearbyShopsHandler"]
|
||||
QH4["GetStaffByShopHandler"]
|
||||
end
|
||||
|
||||
subgraph ReadModel["📊 Read Model"]
|
||||
RM["Dapper / Raw SQL"]
|
||||
end
|
||||
|
||||
DB[("🐘 Database")]
|
||||
|
||||
Q1 --> QH1 --> RM --> DB
|
||||
Q2 --> QH2 --> RM
|
||||
Q3 --> QH3 --> RM
|
||||
Q4 --> QH4 --> RM
|
||||
|
||||
style Q1 fill:#22c55e,stroke:#15803d,color:#fff
|
||||
style Q2 fill:#22c55e,stroke:#15803d,color:#fff
|
||||
style Q3 fill:#22c55e,stroke:#15803d,color:#fff
|
||||
```
|
||||
|
||||
## Database Schema
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
MERCHANTS ||--o{ SHOPS : owns
|
||||
MERCHANTS ||--o{ MERCHANT_STAFF : employs
|
||||
SHOPS ||--o{ SHOP_BRANCHES : has
|
||||
SHOPS ||--o{ SHOP_MEMBERS : assigns
|
||||
MERCHANT_STAFF ||--o{ SHOP_MEMBERS : works_at
|
||||
MERCHANT_STAFF ||--o{ DEVICE_TOKENS : has
|
||||
|
||||
MERCHANTS {
|
||||
uuid id PK
|
||||
uuid user_id FK "IAM.users"
|
||||
uuid organization_id FK "IAM.organizations"
|
||||
varchar business_name
|
||||
int type_id FK
|
||||
int status_id FK
|
||||
varchar tax_id
|
||||
varchar business_license_number
|
||||
varchar bank_account_number
|
||||
decimal commission_rate
|
||||
int settlement_cycle
|
||||
timestamp created_at
|
||||
}
|
||||
|
||||
SHOPS {
|
||||
uuid id PK
|
||||
uuid merchant_id FK
|
||||
varchar name
|
||||
varchar slug UK
|
||||
int type_id FK
|
||||
int category_id FK
|
||||
int status_id FK
|
||||
varchar phone
|
||||
jsonb operating_hours
|
||||
jsonb settings
|
||||
varchar logo_url
|
||||
timestamp created_at
|
||||
}
|
||||
|
||||
SHOP_BRANCHES {
|
||||
uuid id PK
|
||||
uuid shop_id FK
|
||||
varchar name
|
||||
varchar code
|
||||
varchar street
|
||||
varchar district
|
||||
varchar city
|
||||
decimal latitude
|
||||
decimal longitude
|
||||
boolean is_active
|
||||
}
|
||||
|
||||
MERCHANT_STAFF {
|
||||
uuid id PK
|
||||
uuid user_id FK "IAM.users"
|
||||
uuid merchant_id FK
|
||||
int role_id FK
|
||||
int status_id FK
|
||||
varchar employee_code
|
||||
varchar pin_code_hash
|
||||
int permissions
|
||||
timestamp joined_at
|
||||
}
|
||||
|
||||
SHOP_MEMBERS {
|
||||
uuid id PK
|
||||
uuid staff_id FK
|
||||
uuid shop_id FK
|
||||
uuid branch_id FK
|
||||
int shop_role_id FK
|
||||
int custom_permissions
|
||||
boolean is_primary
|
||||
}
|
||||
|
||||
DEVICE_TOKENS {
|
||||
uuid id PK
|
||||
uuid staff_id FK
|
||||
varchar device_id UK
|
||||
varchar device_name
|
||||
varchar fcm_token
|
||||
varchar platform
|
||||
timestamp last_used_at
|
||||
}
|
||||
```
|
||||
|
||||
## Inter-Service Communication
|
||||
|
||||
### Synchronous (HTTP)
|
||||
|
||||
| Target Service | Use Case | Method |
|
||||
|----------------|----------|--------|
|
||||
| IAM Service | Validate user token | `GET /connect/userinfo` |
|
||||
| IAM Service | Assign Merchant role | `POST /api/v1/users/{id}/roles` |
|
||||
| Wallet Service | Check merchant balance | `GET /api/v1/wallets/{merchantId}` |
|
||||
|
||||
### Asynchronous (Events)
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
MERCHANT["🏪 Merchant Service"]
|
||||
BUS["📬 Event Bus"]
|
||||
IAM["🔐 IAM Service"]
|
||||
WALLET["💰 Wallet Service"]
|
||||
NOTIF["🔔 Notification"]
|
||||
|
||||
MERCHANT --> |MerchantApprovedEvent| BUS
|
||||
MERCHANT --> |StaffJoinedEvent| BUS
|
||||
MERCHANT --> |ShopCreatedEvent| BUS
|
||||
|
||||
BUS --> |Subscribe| IAM
|
||||
BUS --> |Subscribe| WALLET
|
||||
BUS --> |Subscribe| NOTIF
|
||||
|
||||
IAM --> |Assign Role| IAM
|
||||
WALLET --> |Create Wallet| WALLET
|
||||
NOTIF --> |Send Email| NOTIF
|
||||
|
||||
style MERCHANT fill:#f97316,stroke:#c2410c,color:#fff
|
||||
style BUS fill:#ef4444,stroke:#b91c1c,color:#fff
|
||||
style IAM fill:#3b82f6,stroke:#1e40af,color:#fff
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
### Authorization
|
||||
|
||||
| Role | Permissions |
|
||||
|------|-------------|
|
||||
| `User` | Register merchant, Accept staff invite |
|
||||
| `Merchant` | Manage own shops, Manage own staff |
|
||||
| `MerchantStaff` | Access POS, View assigned shop data |
|
||||
| `Admin` | Approve/suspend merchants, View all data |
|
||||
|
||||
### POS Authentication
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant POS as POS Device
|
||||
participant API as Merchant Service
|
||||
participant IAM as IAM Service
|
||||
|
||||
POS->>API: POST /api/v1/pos/auth/pin<br/>{userId, pin, deviceId}
|
||||
API->>API: Verify PIN hash
|
||||
|
||||
alt PIN Valid
|
||||
API->>IAM: Validate user & role
|
||||
IAM-->>API: OK
|
||||
API->>API: Generate short-lived token
|
||||
API-->>POS: {posToken, expiresIn: 8h}
|
||||
else PIN Invalid
|
||||
API-->>POS: 401 Unauthorized
|
||||
end
|
||||
```
|
||||
|
||||
## Infrastructure
|
||||
|
||||
### Docker Compose
|
||||
|
||||
```yaml
|
||||
merchant-service-net:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: services/merchant-service-net/Dockerfile
|
||||
environment:
|
||||
- ASPNETCORE_ENVIRONMENT=Development
|
||||
- DATABASE_URL=Host=postgres;Port=5432;Database=merchant;...
|
||||
- Jwt__Authority=http://iam-service-net:8080
|
||||
- IamService__BaseUrl=http://iam-service-net:8080
|
||||
- WalletService__BaseUrl=http://wallet-service-net:8080
|
||||
depends_on:
|
||||
- postgres
|
||||
- iam-service-net
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.merchant.rule=PathPrefix(`/api/v1/merchants`) || PathPrefix(`/api/v1/shops`) || PathPrefix(`/api/v1/pos`)"
|
||||
- "traefik.http.services.merchant.loadbalancer.server.port=8080"
|
||||
```
|
||||
|
||||
### Health Checks
|
||||
|
||||
| Endpoint | Checks | Purpose |
|
||||
|----------|--------|---------|
|
||||
| `/health` | All dependencies | Full health status |
|
||||
| `/health/live` | App is running | Kubernetes liveness |
|
||||
| `/health/ready` | DB + IAM Service | Kubernetes readiness |
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Metrics (Prometheus)
|
||||
|
||||
- `merchant_registrations_total` - Number of merchant registrations
|
||||
- `merchant_approvals_total` - Number of approved merchants
|
||||
- `shop_creations_total` - Number of shops created
|
||||
- `pos_auth_attempts_total` - Number of POS login attempts
|
||||
- `pos_auth_failures_total` - Number of failed POS logins
|
||||
|
||||
### Logging
|
||||
|
||||
Structured Serilog logging with:
|
||||
- Request/Response logging
|
||||
- Domain event logging
|
||||
- POS authentication audit trail
|
||||
- Error tracking with stack traces
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Error Codes
|
||||
|
||||
| Code | HTTP Status | Description |
|
||||
|------|-------------|-------------|
|
||||
| `MERCHANT_NOT_FOUND` | 404 | Merchant does not exist |
|
||||
| `MERCHANT_NOT_ACTIVE` | 400 | Merchant not yet approved |
|
||||
| `SHOP_SLUG_EXISTS` | 409 | Shop slug already exists |
|
||||
| `STAFF_ALREADY_EXISTS` | 409 | Staff already exists |
|
||||
| `INVALID_PIN` | 401 | Incorrect PIN |
|
||||
| `DEVICE_NOT_REGISTERED` | 403 | Device not registered |
|
||||
|
||||
### Response Format
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.4",
|
||||
"title": "Bad Request",
|
||||
"status": 400,
|
||||
"detail": "Only active merchants can create shops.",
|
||||
"errorCode": "MERCHANT_NOT_ACTIVE",
|
||||
"traceId": "00-1234567890abcdef-1234567890abcdef-00"
|
||||
}
|
||||
```
|
||||
303
services/merchant-service-net/docs/en/README.md
Normal file
303
services/merchant-service-net/docs/en/README.md
Normal file
@@ -0,0 +1,303 @@
|
||||
# Merchant Service .NET 10
|
||||
|
||||
> **Service for managing Merchants (Shop Owners), Shops, and Merchant Staff for the GoodGo ecosystem.**
|
||||
|
||||
## Overview
|
||||
|
||||
Merchant Service provides management capabilities for:
|
||||
|
||||
- **Merchant Management** - Registration, verification, merchant (shop owner) management
|
||||
- **Shop Management** - Create and manage stores (Online, Physical, Hybrid)
|
||||
- **Staff Management** - Employee management, permissions for POS System
|
||||
- **Branch Management** - Shop branch/location management
|
||||
- **POS Authentication** - PIN authentication for POS devices
|
||||
- **Multi-category Support** - Support for multiple business categories
|
||||
|
||||
## Requirements
|
||||
|
||||
| Requirement | Version |
|
||||
|-------------|---------|
|
||||
| .NET SDK | 10.0.101+ |
|
||||
| Docker | 24.0+ |
|
||||
| PostgreSQL | 15+ |
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Run with Docker
|
||||
|
||||
```bash
|
||||
cd deployments/local
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Run Local
|
||||
|
||||
```bash
|
||||
cd services/merchant-service-net
|
||||
dotnet restore
|
||||
dotnet build
|
||||
dotnet run --project src/MerchantService.API
|
||||
```
|
||||
|
||||
## Database Migrations
|
||||
|
||||
### Requirements
|
||||
|
||||
```bash
|
||||
# Install EF Core tools (one-time)
|
||||
dotnet tool install --global dotnet-ef
|
||||
```
|
||||
|
||||
### Create Migration
|
||||
|
||||
```bash
|
||||
# Create new migration
|
||||
dotnet ef migrations add <MigrationName> \
|
||||
--project src/MerchantService.Infrastructure \
|
||||
--startup-project src/MerchantService.API
|
||||
```
|
||||
|
||||
### Apply Migration
|
||||
|
||||
```bash
|
||||
# Apply migrations to database
|
||||
dotnet ef database update \
|
||||
--project src/MerchantService.Infrastructure \
|
||||
--startup-project src/MerchantService.API
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Merchant Endpoints (`/api/v1/merchants`)
|
||||
|
||||
| Method | Endpoint | Description | Auth |
|
||||
|--------|----------|-------------|------|
|
||||
| `POST` | `/api/v1/merchants/register` | Register as a Merchant | ✅ User |
|
||||
| `GET` | `/api/v1/merchants/me` | Get current Merchant info | ✅ Merchant |
|
||||
| `PUT` | `/api/v1/merchants/me` | Update Merchant info | ✅ Merchant |
|
||||
| `POST` | `/api/v1/merchants/me/verify` | Submit verification documents | ✅ Merchant |
|
||||
|
||||
### Shop Endpoints (`/api/v1/shops`)
|
||||
|
||||
| Method | Endpoint | Description | Auth |
|
||||
|--------|----------|-------------|------|
|
||||
| `GET` | `/api/v1/shops` | List Merchant's shops | ✅ Merchant |
|
||||
| `POST` | `/api/v1/shops` | Create new shop | ✅ Merchant |
|
||||
| `GET` | `/api/v1/shops/{id}` | Get shop by ID | ✅ Merchant |
|
||||
| `PUT` | `/api/v1/shops/{id}` | Update shop | ✅ Merchant |
|
||||
| `DELETE` | `/api/v1/shops/{id}` | Close shop | ✅ Merchant |
|
||||
| `GET` | `/api/v1/shops/slug/{slug}` | Get shop by slug (public) | ❌ |
|
||||
|
||||
### Shop Branch Endpoints
|
||||
|
||||
| Method | Endpoint | Description | Auth |
|
||||
|--------|----------|-------------|------|
|
||||
| `GET` | `/api/v1/shops/{shopId}/branches` | List branches | ✅ |
|
||||
| `POST` | `/api/v1/shops/{shopId}/branches` | Create new branch | ✅ Merchant |
|
||||
| `PUT` | `/api/v1/shops/{shopId}/branches/{id}` | Update branch | ✅ Merchant |
|
||||
| `DELETE` | `/api/v1/shops/{shopId}/branches/{id}` | Delete branch | ✅ Merchant |
|
||||
| `GET` | `/api/v1/shops/nearby` | Find nearby shops | ❌ |
|
||||
|
||||
### Merchant Staff Endpoints
|
||||
|
||||
| Method | Endpoint | Description | Auth |
|
||||
|--------|----------|-------------|------|
|
||||
| `GET` | `/api/v1/merchants/me/staff` | List staff | ✅ Merchant |
|
||||
| `POST` | `/api/v1/merchants/me/staff/invite` | Invite staff | ✅ Merchant |
|
||||
| `PUT` | `/api/v1/merchants/me/staff/{id}` | Update staff | ✅ Merchant |
|
||||
| `DELETE` | `/api/v1/merchants/me/staff/{id}` | Remove staff | ✅ Merchant |
|
||||
| `POST` | `/api/v1/staff/accept-invite` | Accept invitation | ✅ User |
|
||||
|
||||
### POS Endpoints (`/api/v1/pos`)
|
||||
|
||||
| Method | Endpoint | Description | Auth |
|
||||
|--------|----------|-------------|------|
|
||||
| `POST` | `/api/v1/pos/auth/pin` | POS login with PIN | ✅ Staff |
|
||||
| `POST` | `/api/v1/pos/devices/register` | Register POS device | ✅ Staff |
|
||||
| `GET` | `/api/v1/pos/me` | Get current Staff info | ✅ Staff |
|
||||
|
||||
### Admin Endpoints
|
||||
|
||||
| Method | Endpoint | Description | Auth |
|
||||
|--------|----------|-------------|------|
|
||||
| `GET` | `/api/v1/admin/merchants` | List all merchants | ✅ Admin |
|
||||
| `POST` | `/api/v1/admin/merchants/{id}/approve` | Approve merchant | ✅ Admin |
|
||||
| `POST` | `/api/v1/admin/merchants/{id}/suspend` | Suspend merchant | ✅ Admin |
|
||||
|
||||
### Health Checks
|
||||
|
||||
| Endpoint | Purpose |
|
||||
|----------|---------|
|
||||
| `/health` | Full health status |
|
||||
| `/health/live` | Liveness check |
|
||||
| `/health/ready` | Readiness check |
|
||||
|
||||
## Merchant Registration Flow
|
||||
|
||||
### Step 1: Register as Merchant
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:5003/api/v1/merchants/register \
|
||||
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"businessName": "Saigon Coffee",
|
||||
"type": "Company",
|
||||
"taxId": "0123456789",
|
||||
"businessLicenseNumber": "BL-2024-001"
|
||||
}'
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"merchantId": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"businessName": "Saigon Coffee",
|
||||
"status": "PendingApproval"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Wait for Admin Approval
|
||||
|
||||
Admin will review and approve the merchant. After approval:
|
||||
- Role `Merchant` will be assigned to the user in IAM Service
|
||||
- Status changes to `Active`
|
||||
|
||||
### Step 3: Create Shop
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:5003/api/v1/shops \
|
||||
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Saigon Coffee - District 1",
|
||||
"slug": "saigon-coffee-d1",
|
||||
"type": "Hybrid",
|
||||
"category": "FoodBeverage",
|
||||
"phone": "0901234567",
|
||||
"email": "d1@saigoncoffee.vn"
|
||||
}'
|
||||
```
|
||||
|
||||
### Step 4: Add Branch (For physical stores)
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:5003/api/v1/shops/{shopId}/branches \
|
||||
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Le Loi Branch",
|
||||
"street": "123 Le Loi Street",
|
||||
"district": "District 1",
|
||||
"city": "Ho Chi Minh City",
|
||||
"latitude": 10.7769,
|
||||
"longitude": 106.7009
|
||||
}'
|
||||
```
|
||||
|
||||
### Step 5: Invite Staff
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:5003/api/v1/merchants/me/staff/invite \
|
||||
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "staff@example.com",
|
||||
"role": "Cashier",
|
||||
"shopId": "shop-uuid",
|
||||
"branchId": "branch-uuid"
|
||||
}'
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `ASPNETCORE_ENVIRONMENT` | Environment | `Development` |
|
||||
| `DATABASE_URL` | PostgreSQL connection | - |
|
||||
| `Jwt__Authority` | IAM Service URL | `http://iam-service-net:8080` |
|
||||
| `IamService__BaseUrl` | IAM Service base URL | - |
|
||||
| `WalletService__BaseUrl` | Wallet Service base URL | - |
|
||||
|
||||
### appsettings.json
|
||||
|
||||
```json
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Host=localhost;Port=5432;Database=merchant;Username=postgres;Password=postgres"
|
||||
},
|
||||
"Jwt": {
|
||||
"Authority": "http://localhost:5001"
|
||||
},
|
||||
"IamService": {
|
||||
"BaseUrl": "http://localhost:5001"
|
||||
},
|
||||
"WalletService": {
|
||||
"BaseUrl": "http://localhost:5004"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Integration with Other Services
|
||||
|
||||
### IAM Service
|
||||
- Validate User token
|
||||
- Assign Merchant/MerchantStaff roles
|
||||
- Get Organization hierarchy
|
||||
|
||||
### Wallet Service
|
||||
- Merchant settlement
|
||||
- Commission calculation
|
||||
- Balance check
|
||||
|
||||
### Membership Service
|
||||
- Customer visit tracking → Add EXP
|
||||
- Member level → Special discount at shop
|
||||
|
||||
### Chat Service
|
||||
- Shop support channel
|
||||
- Staff-Customer conversation
|
||||
|
||||
### Gift Voucher Service (Planned)
|
||||
- Create voucher campaigns
|
||||
- Voucher redemption at shop
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Unit tests
|
||||
dotnet test tests/MerchantService.UnitTests
|
||||
|
||||
# Functional tests
|
||||
dotnet test tests/MerchantService.FunctionalTests
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### Docker Build
|
||||
|
||||
```bash
|
||||
docker build -t goodgo/merchant-service:latest .
|
||||
docker run -p 5003:8080 --env-file .env goodgo/merchant-service:latest
|
||||
```
|
||||
|
||||
### Kubernetes
|
||||
|
||||
See `deployments/kubernetes/merchant-service.yaml`
|
||||
|
||||
## Swagger UI
|
||||
|
||||
After running the service, access Swagger UI at:
|
||||
- **Local**: http://localhost:5003/swagger
|
||||
- **Docker**: http://localhost/api/v1/merchants/swagger
|
||||
|
||||
## Resources
|
||||
|
||||
- [Clean Architecture Documentation](../../docs/en/architecture/)
|
||||
- [API Design Skill](../../.agent/skills/api-design/SKILL.md)
|
||||
- [CQRS Pattern Skill](../../.agent/skills/cqrs-mediatr/SKILL.md)
|
||||
803
services/merchant-service-net/docs/vi/ARCHITECTURE.md
Normal file
803
services/merchant-service-net/docs/vi/ARCHITECTURE.md
Normal file
@@ -0,0 +1,803 @@
|
||||
# Kiến Trúc Merchant Service
|
||||
|
||||
## Tổng Quan Hệ Thống
|
||||
|
||||
Merchant Service quản lý **Merchant (Shop Owner)**, **Shop**, và **Merchant Staff** cho hệ sinh thái GoodGo. Service này hỗ trợ đa loại hình kinh doanh: cửa hàng online, cửa hàng vật lý, và hybrid.
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph Client["Client Layer"]
|
||||
WEB["🌐 Merchant Portal"]
|
||||
POS["💳 POS System"]
|
||||
MOBILE["📱 Customer App"]
|
||||
end
|
||||
|
||||
subgraph Gateway["API Gateway"]
|
||||
TRAEFIK["⚡ Traefik"]
|
||||
end
|
||||
|
||||
subgraph Services["Microservices"]
|
||||
IAM["🔐 IAM Service<br/>(User & Roles)"]
|
||||
MERCHANT["🏪 Merchant Service<br/>(Shop, Staff)"]
|
||||
WALLET["💰 Wallet Service"]
|
||||
MEMBER["👥 Membership Service"]
|
||||
VOUCHER["🎁 Voucher Service"]
|
||||
end
|
||||
|
||||
subgraph Data["Data Layer"]
|
||||
PG[("🐘 PostgreSQL")]
|
||||
end
|
||||
|
||||
WEB --> TRAEFIK
|
||||
POS --> TRAEFIK
|
||||
MOBILE --> TRAEFIK
|
||||
TRAEFIK --> |"/api/v1/auth"| IAM
|
||||
TRAEFIK --> |"/api/v1/merchants<br/>/api/v1/shops"| MERCHANT
|
||||
TRAEFIK --> |"/api/v1/wallets"| WALLET
|
||||
|
||||
IAM --> |"JWT Token"| MERCHANT
|
||||
MERCHANT --> |"Assign Roles"| IAM
|
||||
MERCHANT --> |"Settlement"| WALLET
|
||||
MERCHANT -.-> |"Customer Visit"| MEMBER
|
||||
MERCHANT -.-> |"Voucher Redemption"| VOUCHER
|
||||
MERCHANT --> PG
|
||||
|
||||
style IAM fill:#3b82f6,stroke:#1e40af,color:#fff
|
||||
style MERCHANT fill:#f97316,stroke:#c2410c,color:#fff
|
||||
style WALLET fill:#a855f7,stroke:#7e22ce,color:#fff
|
||||
style MEMBER fill:#22c55e,stroke:#15803d,color:#fff
|
||||
style VOUCHER fill:#ec4899,stroke:#be185d,color:#fff
|
||||
style TRAEFIK fill:#6366f1,stroke:#4338ca,color:#fff
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Phân chia trách nhiệm:**
|
||||
> - **IAM Service**: Quản lý User identity, Roles (Merchant, MerchantStaff)
|
||||
> - **Merchant Service**: Quản lý Merchant, Shop, ShopBranch, MerchantStaff
|
||||
> - **Wallet Service**: Settlement, Commission calculation
|
||||
> - **Voucher Service**: Gift Voucher (planned)
|
||||
|
||||
## Các Lớp Clean Architecture
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ Lớp API (Presentation) │
|
||||
│ Controllers, Commands, Queries, Validators, DTOs │
|
||||
├────────────────────────────────────────────────────────────┤
|
||||
│ Lớp Domain (Core) │
|
||||
│ Entities, Aggregates, Value Objects, Domain Events │
|
||||
│ Merchant, Shop, MerchantStaff Aggregates │
|
||||
├────────────────────────────────────────────────────────────┤
|
||||
│ Lớp Infrastructure │
|
||||
│ DbContext, Repositories, Entity Configurations │
|
||||
│ External Services (IAM Client, Wallet Client) │
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Domain Model
|
||||
|
||||
### Tổng Quan Aggregates
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class Merchant {
|
||||
+Guid Id
|
||||
+Guid UserId
|
||||
+Guid? OrganizationId
|
||||
+string BusinessName
|
||||
+MerchantType Type
|
||||
+MerchantStatus Status
|
||||
+BusinessInfo BusinessInfo
|
||||
+SettlementConfig SettlementConfig
|
||||
+Register()
|
||||
+Approve()
|
||||
+Suspend()
|
||||
+CreateShop()
|
||||
}
|
||||
|
||||
class Shop {
|
||||
+Guid Id
|
||||
+Guid MerchantId
|
||||
+string Name
|
||||
+string Slug
|
||||
+ShopType Type
|
||||
+BusinessCategory Category
|
||||
+ShopStatus Status
|
||||
+ContactInfo ContactInfo
|
||||
+OperatingHours? Hours
|
||||
+AddBranch()
|
||||
+AssignStaff()
|
||||
+UpdateSettings()
|
||||
}
|
||||
|
||||
class ShopBranch {
|
||||
+Guid Id
|
||||
+Guid ShopId
|
||||
+string Name
|
||||
+string Code
|
||||
+Address Address
|
||||
+GeoLocation? Location
|
||||
+bool IsActive
|
||||
}
|
||||
|
||||
class MerchantStaff {
|
||||
+Guid Id
|
||||
+Guid UserId
|
||||
+Guid MerchantId
|
||||
+StaffRole Role
|
||||
+StaffStatus Status
|
||||
+string? EmployeeCode
|
||||
+StaffPermissions Permissions
|
||||
+SetPinCode()
|
||||
+RegisterDevice()
|
||||
}
|
||||
|
||||
class ShopMember {
|
||||
+Guid StaffId
|
||||
+Guid ShopId
|
||||
+Guid? BranchId
|
||||
+ShopRole Role
|
||||
+int? CustomPermissions
|
||||
}
|
||||
|
||||
Merchant "1" --> "*" Shop : owns
|
||||
Merchant "1" --> "*" MerchantStaff : employs
|
||||
Shop "1" --> "*" ShopBranch : has
|
||||
Shop "1" --> "*" ShopMember : assigns
|
||||
MerchantStaff "1" --> "*" ShopMember : works at
|
||||
|
||||
style Merchant fill:#f97316,stroke:#c2410c,color:#fff
|
||||
style Shop fill:#3b82f6,stroke:#1e40af,color:#fff
|
||||
style MerchantStaff fill:#22c55e,stroke:#15803d,color:#fff
|
||||
```
|
||||
|
||||
### Merchant Aggregate (Aggregate Root)
|
||||
|
||||
```csharp
|
||||
public class Merchant : Entity, IAggregateRoot
|
||||
{
|
||||
private Guid _userId; // Reference to IAM User (Owner)
|
||||
private Guid? _organizationId; // Reference to IAM Organization
|
||||
private string _businessName;
|
||||
private MerchantType _type; // Individual, Company
|
||||
private MerchantStatus _status; // Pending, Active, Suspended, Banned
|
||||
private MerchantVerification _verification;
|
||||
private SettlementConfig _settlementConfig;
|
||||
|
||||
// Value Object for business details
|
||||
public BusinessInfo BusinessInfo { get; private set; }
|
||||
|
||||
// Collections
|
||||
private readonly List<Shop> _shops = new();
|
||||
private readonly List<MerchantStaff> _staff = new();
|
||||
|
||||
public IReadOnlyCollection<Shop> Shops => _shops.AsReadOnly();
|
||||
public IReadOnlyCollection<MerchantStaff> Staff => _staff.AsReadOnly();
|
||||
|
||||
/// <summary>
|
||||
/// EN: Factory method to register a new merchant.
|
||||
/// VI: Factory method để đăng ký merchant mới.
|
||||
/// </summary>
|
||||
public static Merchant Register(Guid userId, string businessName, MerchantType type)
|
||||
{
|
||||
var merchant = new Merchant
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
_userId = userId,
|
||||
_businessName = businessName,
|
||||
_type = type,
|
||||
_status = MerchantStatus.PendingApproval,
|
||||
_verification = new MerchantVerification()
|
||||
};
|
||||
|
||||
merchant.AddDomainEvent(new MerchantRegisteredDomainEvent(merchant));
|
||||
return merchant;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Approve merchant application.
|
||||
/// VI: Phê duyệt đơn đăng ký merchant.
|
||||
/// </summary>
|
||||
public void Approve(Guid approvedBy)
|
||||
{
|
||||
if (_status != MerchantStatus.PendingApproval)
|
||||
throw new DomainException("Can only approve pending merchants");
|
||||
|
||||
_status = MerchantStatus.Active;
|
||||
AddDomainEvent(new MerchantApprovedDomainEvent(this, approvedBy));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Create a new shop under this merchant.
|
||||
/// VI: Tạo shop mới thuộc merchant này.
|
||||
/// </summary>
|
||||
public Shop CreateShop(string name, string slug, ShopType type, BusinessCategory category)
|
||||
{
|
||||
if (_status != MerchantStatus.Active)
|
||||
throw new DomainException("Only active merchants can create shops");
|
||||
|
||||
var shop = new Shop(Id, name, slug, type, category);
|
||||
_shops.Add(shop);
|
||||
return shop;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Shop Aggregate (Aggregate Root)
|
||||
|
||||
```csharp
|
||||
public class Shop : Entity, IAggregateRoot
|
||||
{
|
||||
private Guid _merchantId;
|
||||
private string _name;
|
||||
private string _slug;
|
||||
private ShopType _type;
|
||||
private BusinessCategory _category;
|
||||
private ShopStatus _status;
|
||||
private ContactInfo _contactInfo;
|
||||
private OperatingHours? _operatingHours;
|
||||
private ShopSettings _settings;
|
||||
|
||||
// Physical locations
|
||||
private readonly List<ShopBranch> _branches = new();
|
||||
|
||||
// Staff assignments
|
||||
private readonly List<ShopMember> _members = new();
|
||||
|
||||
public IReadOnlyCollection<ShopBranch> Branches => _branches.AsReadOnly();
|
||||
public IReadOnlyCollection<ShopMember> Members => _members.AsReadOnly();
|
||||
|
||||
/// <summary>
|
||||
/// EN: Add a physical branch to the shop.
|
||||
/// VI: Thêm chi nhánh vật lý cho shop.
|
||||
/// </summary>
|
||||
public ShopBranch AddBranch(string name, Address address, GeoLocation? location = null)
|
||||
{
|
||||
if (_type == ShopType.OnlineOnly)
|
||||
throw new DomainException("Online-only shops cannot have physical branches");
|
||||
|
||||
var branch = new ShopBranch(Id, name, address, location);
|
||||
_branches.Add(branch);
|
||||
AddDomainEvent(new ShopBranchAddedDomainEvent(this, branch));
|
||||
return branch;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Assign a staff member to this shop.
|
||||
/// VI: Gán nhân viên cho shop này.
|
||||
/// </summary>
|
||||
public void AssignStaff(MerchantStaff staff, ShopRole role, Guid? branchId = null)
|
||||
{
|
||||
var member = new ShopMember(staff.Id, Id, role, branchId);
|
||||
_members.Add(member);
|
||||
AddDomainEvent(new StaffAssignedToShopDomainEvent(staff, this, role));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### MerchantStaff Aggregate (Aggregate Root)
|
||||
|
||||
```csharp
|
||||
public class MerchantStaff : Entity, IAggregateRoot
|
||||
{
|
||||
private Guid _userId; // Reference to IAM User
|
||||
private Guid _merchantId; // Employer
|
||||
private StaffRole _role; // Cashier, Manager, Admin
|
||||
private StaffStatus _status;
|
||||
private string? _employeeCode;
|
||||
private ContactInfo _contactInfo;
|
||||
|
||||
// For POS System quick authentication
|
||||
private readonly List<DeviceToken> _deviceTokens = new();
|
||||
private string? _pinCodeHash; // Hashed 4-6 digit PIN
|
||||
|
||||
// Permissions bitmask
|
||||
private StaffPermissions _permissions;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Set PIN code for POS authentication.
|
||||
/// VI: Đặt mã PIN cho xác thực POS.
|
||||
/// </summary>
|
||||
public void SetPinCode(string pin)
|
||||
{
|
||||
if (pin.Length < 4 || pin.Length > 6 || !pin.All(char.IsDigit))
|
||||
throw new DomainException("PIN must be 4-6 digits");
|
||||
|
||||
_pinCodeHash = HashPin(pin);
|
||||
AddDomainEvent(new StaffPinCodeSetDomainEvent(this));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Verify PIN code for POS login.
|
||||
/// VI: Xác minh mã PIN cho đăng nhập POS.
|
||||
/// </summary>
|
||||
public bool VerifyPinCode(string pin)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_pinCodeHash))
|
||||
return false;
|
||||
|
||||
return VerifyHash(pin, _pinCodeHash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Register a device for push notifications.
|
||||
/// VI: Đăng ký thiết bị cho push notifications.
|
||||
/// </summary>
|
||||
public void RegisterDevice(string deviceId, string deviceName, string fcmToken)
|
||||
{
|
||||
var existingToken = _deviceTokens.FirstOrDefault(t => t.DeviceId == deviceId);
|
||||
if (existingToken != null)
|
||||
{
|
||||
existingToken.Update(fcmToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
var token = new DeviceToken(deviceId, deviceName, fcmToken);
|
||||
_deviceTokens.Add(token);
|
||||
}
|
||||
|
||||
AddDomainEvent(new StaffDeviceRegisteredDomainEvent(this, deviceId));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Value Objects
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// EN: Business information for merchant verification.
|
||||
/// VI: Thông tin doanh nghiệp để xác minh merchant.
|
||||
/// </summary>
|
||||
public record BusinessInfo(
|
||||
string? TaxId,
|
||||
string? BusinessLicenseNumber,
|
||||
string? CompanyRegistrationNumber,
|
||||
DateTime? EstablishedDate
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Settlement configuration for merchant payouts.
|
||||
/// VI: Cấu hình thanh toán cho chi trả merchant.
|
||||
/// </summary>
|
||||
public record SettlementConfig(
|
||||
BankAccount BankAccount,
|
||||
decimal CommissionRate, // Platform fee (%)
|
||||
SettlementCycle Cycle, // Daily, Weekly, Monthly
|
||||
bool AutoSettlement
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Bank account for settlement.
|
||||
/// VI: Tài khoản ngân hàng để thanh toán.
|
||||
/// </summary>
|
||||
public record BankAccount(
|
||||
string BankCode,
|
||||
string BankName,
|
||||
string AccountNumber,
|
||||
string AccountHolderName
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Physical address.
|
||||
/// VI: Địa chỉ vật lý.
|
||||
/// </summary>
|
||||
public record Address(
|
||||
string Street,
|
||||
string Ward,
|
||||
string District,
|
||||
string City,
|
||||
string Province,
|
||||
string PostalCode,
|
||||
string CountryCode = "VN"
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Geographic location for map display and nearby search.
|
||||
/// VI: Vị trí địa lý để hiển thị bản đồ và tìm kiếm gần đây.
|
||||
/// </summary>
|
||||
public record GeoLocation(double Latitude, double Longitude);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Contact information.
|
||||
/// VI: Thông tin liên hệ.
|
||||
/// </summary>
|
||||
public record ContactInfo(
|
||||
string Phone,
|
||||
string? Email,
|
||||
string? Website
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Operating hours for physical shops.
|
||||
/// VI: Giờ hoạt động cho cửa hàng vật lý.
|
||||
/// </summary>
|
||||
public record OperatingHours(
|
||||
TimeOnly OpenTime,
|
||||
TimeOnly CloseTime,
|
||||
List<DayOfWeek> OpenDays
|
||||
);
|
||||
```
|
||||
|
||||
### Enumerations
|
||||
|
||||
```csharp
|
||||
// Merchant Types
|
||||
public class MerchantType : Enumeration
|
||||
{
|
||||
public static readonly MerchantType Individual = new(1, "Individual"); // Cá nhân
|
||||
public static readonly MerchantType Company = new(2, "Company"); // Doanh nghiệp
|
||||
}
|
||||
|
||||
// Merchant Status
|
||||
public class MerchantStatus : Enumeration
|
||||
{
|
||||
public static readonly MerchantStatus PendingApproval = new(1, "PendingApproval");
|
||||
public static readonly MerchantStatus Active = new(2, "Active");
|
||||
public static readonly MerchantStatus Suspended = new(3, "Suspended");
|
||||
public static readonly MerchantStatus Banned = new(4, "Banned");
|
||||
}
|
||||
|
||||
// Shop Types
|
||||
public class ShopType : Enumeration
|
||||
{
|
||||
public static readonly ShopType OnlineOnly = new(1, "OnlineOnly"); // Chỉ online
|
||||
public static readonly ShopType PhysicalOnly = new(2, "PhysicalOnly"); // Chỉ cửa hàng
|
||||
public static readonly ShopType Hybrid = new(3, "Hybrid"); // Cả hai
|
||||
}
|
||||
|
||||
// Business Categories (Ngành nghề)
|
||||
public class BusinessCategory : Enumeration
|
||||
{
|
||||
public static readonly BusinessCategory FoodBeverage = new(1, "FoodBeverage");
|
||||
public static readonly BusinessCategory Fashion = new(2, "Fashion");
|
||||
public static readonly BusinessCategory Electronics = new(3, "Electronics");
|
||||
public static readonly BusinessCategory Healthcare = new(4, "Healthcare");
|
||||
public static readonly BusinessCategory Beauty = new(5, "Beauty");
|
||||
public static readonly BusinessCategory Education = new(6, "Education");
|
||||
public static readonly BusinessCategory Entertainment = new(7, "Entertainment");
|
||||
public static readonly BusinessCategory Services = new(8, "Services");
|
||||
public static readonly BusinessCategory Other = new(99, "Other");
|
||||
}
|
||||
|
||||
// Shop Roles (for Staff assignment at shop level)
|
||||
public class ShopRole : Enumeration
|
||||
{
|
||||
public static readonly ShopRole Cashier = new(1, "Cashier"); // Thu ngân
|
||||
public static readonly ShopRole Waiter = new(2, "Waiter"); // Phục vụ
|
||||
public static readonly ShopRole Manager = new(3, "Manager"); // Quản lý
|
||||
public static readonly ShopRole Owner = new(4, "Owner"); // Chủ shop
|
||||
}
|
||||
|
||||
// Staff Permissions (Bitmask)
|
||||
[Flags]
|
||||
public enum StaffPermissions
|
||||
{
|
||||
None = 0,
|
||||
ViewSales = 1,
|
||||
ProcessPayment = 2,
|
||||
RefundOrder = 4,
|
||||
ManageInventory = 8,
|
||||
ViewReports = 16,
|
||||
ManageStaff = 32,
|
||||
ManageSettings = 64,
|
||||
All = int.MaxValue
|
||||
}
|
||||
```
|
||||
|
||||
### Domain Events
|
||||
|
||||
| Event | Trigger | Mục Đích |
|
||||
|-------|---------|----------|
|
||||
| `MerchantRegisteredDomainEvent` | Đăng ký merchant mới | Notify admin for review |
|
||||
| `MerchantApprovedDomainEvent` | Admin phê duyệt | **Assign "Merchant" role via IAM** |
|
||||
| `MerchantSuspendedDomainEvent` | Tạm ngưng merchant | Notify merchant, disable shops |
|
||||
| `ShopCreatedDomainEvent` | Tạo shop mới | Analytics, indexing |
|
||||
| `ShopBranchAddedDomainEvent` | Thêm chi nhánh | Geo-indexing for nearby search |
|
||||
| `StaffInvitedDomainEvent` | Mời nhân viên | Send invitation email |
|
||||
| `StaffJoinedDomainEvent` | Nhân viên chấp nhận | **Assign "MerchantStaff" role via IAM** |
|
||||
| `StaffDeviceRegisteredDomainEvent` | Đăng ký thiết bị POS | Enable push notifications |
|
||||
|
||||
## CQRS Pattern
|
||||
|
||||
### Write Side (Commands)
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Commands["✏️ Commands"]
|
||||
CMD1["RegisterMerchantCommand"]
|
||||
CMD2["CreateShopCommand"]
|
||||
CMD3["AddShopBranchCommand"]
|
||||
CMD4["InviteStaffCommand"]
|
||||
CMD5["SetStaffPinCommand"]
|
||||
end
|
||||
|
||||
subgraph Handlers["⚙️ Command Handlers"]
|
||||
H1["RegisterMerchantHandler"]
|
||||
H2["CreateShopHandler"]
|
||||
H3["AddShopBranchHandler"]
|
||||
H4["InviteStaffHandler"]
|
||||
H5["SetStaffPinHandler"]
|
||||
end
|
||||
|
||||
subgraph Domain["🏛️ Domain Model"]
|
||||
DM["EF Core<br/>Repository"]
|
||||
end
|
||||
|
||||
DB[("🐘 Database")]
|
||||
|
||||
CMD1 --> H1 --> DM --> DB
|
||||
CMD2 --> H2 --> DM
|
||||
CMD3 --> H3 --> DM
|
||||
CMD4 --> H4 --> DM
|
||||
CMD5 --> H5 --> DM
|
||||
|
||||
style CMD1 fill:#ef4444,stroke:#b91c1c,color:#fff
|
||||
style CMD2 fill:#ef4444,stroke:#b91c1c,color:#fff
|
||||
style CMD3 fill:#ef4444,stroke:#b91c1c,color:#fff
|
||||
```
|
||||
|
||||
### Read Side (Queries)
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Queries["📖 Queries"]
|
||||
Q1["GetMerchantByIdQuery"]
|
||||
Q2["GetShopsByMerchantQuery"]
|
||||
Q3["GetNearbyShopsQuery"]
|
||||
Q4["GetStaffByShopQuery"]
|
||||
end
|
||||
|
||||
subgraph Handlers["🔍 Query Handlers"]
|
||||
QH1["GetMerchantByIdHandler"]
|
||||
QH2["GetShopsByMerchantHandler"]
|
||||
QH3["GetNearbyShopsHandler"]
|
||||
QH4["GetStaffByShopHandler"]
|
||||
end
|
||||
|
||||
subgraph ReadModel["📊 Read Model"]
|
||||
RM["Dapper / Raw SQL"]
|
||||
end
|
||||
|
||||
DB[("🐘 Database")]
|
||||
|
||||
Q1 --> QH1 --> RM --> DB
|
||||
Q2 --> QH2 --> RM
|
||||
Q3 --> QH3 --> RM
|
||||
Q4 --> QH4 --> RM
|
||||
|
||||
style Q1 fill:#22c55e,stroke:#15803d,color:#fff
|
||||
style Q2 fill:#22c55e,stroke:#15803d,color:#fff
|
||||
style Q3 fill:#22c55e,stroke:#15803d,color:#fff
|
||||
```
|
||||
|
||||
## Database Schema
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
MERCHANTS ||--o{ SHOPS : owns
|
||||
MERCHANTS ||--o{ MERCHANT_STAFF : employs
|
||||
SHOPS ||--o{ SHOP_BRANCHES : has
|
||||
SHOPS ||--o{ SHOP_MEMBERS : assigns
|
||||
MERCHANT_STAFF ||--o{ SHOP_MEMBERS : works_at
|
||||
MERCHANT_STAFF ||--o{ DEVICE_TOKENS : has
|
||||
|
||||
MERCHANTS {
|
||||
uuid id PK
|
||||
uuid user_id FK "IAM.users"
|
||||
uuid organization_id FK "IAM.organizations"
|
||||
varchar business_name
|
||||
int type_id FK
|
||||
int status_id FK
|
||||
varchar tax_id
|
||||
varchar business_license_number
|
||||
varchar bank_account_number
|
||||
decimal commission_rate
|
||||
int settlement_cycle
|
||||
timestamp created_at
|
||||
}
|
||||
|
||||
SHOPS {
|
||||
uuid id PK
|
||||
uuid merchant_id FK
|
||||
varchar name
|
||||
varchar slug UK
|
||||
int type_id FK
|
||||
int category_id FK
|
||||
int status_id FK
|
||||
varchar phone
|
||||
jsonb operating_hours
|
||||
jsonb settings
|
||||
varchar logo_url
|
||||
timestamp created_at
|
||||
}
|
||||
|
||||
SHOP_BRANCHES {
|
||||
uuid id PK
|
||||
uuid shop_id FK
|
||||
varchar name
|
||||
varchar code
|
||||
varchar street
|
||||
varchar district
|
||||
varchar city
|
||||
decimal latitude
|
||||
decimal longitude
|
||||
boolean is_active
|
||||
}
|
||||
|
||||
MERCHANT_STAFF {
|
||||
uuid id PK
|
||||
uuid user_id FK "IAM.users"
|
||||
uuid merchant_id FK
|
||||
int role_id FK
|
||||
int status_id FK
|
||||
varchar employee_code
|
||||
varchar pin_code_hash
|
||||
int permissions
|
||||
timestamp joined_at
|
||||
}
|
||||
|
||||
SHOP_MEMBERS {
|
||||
uuid id PK
|
||||
uuid staff_id FK
|
||||
uuid shop_id FK
|
||||
uuid branch_id FK
|
||||
int shop_role_id FK
|
||||
int custom_permissions
|
||||
boolean is_primary
|
||||
}
|
||||
|
||||
DEVICE_TOKENS {
|
||||
uuid id PK
|
||||
uuid staff_id FK
|
||||
varchar device_id UK
|
||||
varchar device_name
|
||||
varchar fcm_token
|
||||
varchar platform
|
||||
timestamp last_used_at
|
||||
}
|
||||
```
|
||||
|
||||
## Inter-Service Communication
|
||||
|
||||
### Synchronous (HTTP)
|
||||
|
||||
| Target Service | Use Case | Method |
|
||||
|----------------|----------|--------|
|
||||
| IAM Service | Validate user token | `GET /connect/userinfo` |
|
||||
| IAM Service | Assign Merchant role | `POST /api/v1/users/{id}/roles` |
|
||||
| Wallet Service | Check merchant balance | `GET /api/v1/wallets/{merchantId}` |
|
||||
|
||||
### Asynchronous (Events)
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
MERCHANT["🏪 Merchant Service"]
|
||||
BUS["📬 Event Bus"]
|
||||
IAM["🔐 IAM Service"]
|
||||
WALLET["💰 Wallet Service"]
|
||||
NOTIF["🔔 Notification"]
|
||||
|
||||
MERCHANT --> |MerchantApprovedEvent| BUS
|
||||
MERCHANT --> |StaffJoinedEvent| BUS
|
||||
MERCHANT --> |ShopCreatedEvent| BUS
|
||||
|
||||
BUS --> |Subscribe| IAM
|
||||
BUS --> |Subscribe| WALLET
|
||||
BUS --> |Subscribe| NOTIF
|
||||
|
||||
IAM --> |Assign Role| IAM
|
||||
WALLET --> |Create Wallet| WALLET
|
||||
NOTIF --> |Send Email| NOTIF
|
||||
|
||||
style MERCHANT fill:#f97316,stroke:#c2410c,color:#fff
|
||||
style BUS fill:#ef4444,stroke:#b91c1c,color:#fff
|
||||
style IAM fill:#3b82f6,stroke:#1e40af,color:#fff
|
||||
```
|
||||
|
||||
## Bảo Mật
|
||||
|
||||
### Authorization
|
||||
|
||||
| Role | Permissions |
|
||||
|------|-------------|
|
||||
| `User` | Register merchant, Accept staff invite |
|
||||
| `Merchant` | Manage own shops, Manage own staff |
|
||||
| `MerchantStaff` | Access POS, View assigned shop data |
|
||||
| `Admin` | Approve/suspend merchants, View all data |
|
||||
|
||||
### POS Authentication
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant POS as POS Device
|
||||
participant API as Merchant Service
|
||||
participant IAM as IAM Service
|
||||
|
||||
POS->>API: POST /api/v1/pos/auth/pin<br/>{userId, pin, deviceId}
|
||||
API->>API: Verify PIN hash
|
||||
|
||||
alt PIN Valid
|
||||
API->>IAM: Validate user & role
|
||||
IAM-->>API: OK
|
||||
API->>API: Generate short-lived token
|
||||
API-->>POS: {posToken, expiresIn: 8h}
|
||||
else PIN Invalid
|
||||
API-->>POS: 401 Unauthorized
|
||||
end
|
||||
```
|
||||
|
||||
## Hạ Tầng
|
||||
|
||||
### Docker Compose
|
||||
|
||||
```yaml
|
||||
merchant-service-net:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: services/merchant-service-net/Dockerfile
|
||||
environment:
|
||||
- ASPNETCORE_ENVIRONMENT=Development
|
||||
- DATABASE_URL=Host=postgres;Port=5432;Database=merchant;...
|
||||
- Jwt__Authority=http://iam-service-net:8080
|
||||
- IamService__BaseUrl=http://iam-service-net:8080
|
||||
- WalletService__BaseUrl=http://wallet-service-net:8080
|
||||
depends_on:
|
||||
- postgres
|
||||
- iam-service-net
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.merchant.rule=PathPrefix(`/api/v1/merchants`) || PathPrefix(`/api/v1/shops`) || PathPrefix(`/api/v1/pos`)"
|
||||
- "traefik.http.services.merchant.loadbalancer.server.port=8080"
|
||||
```
|
||||
|
||||
### Health Checks
|
||||
|
||||
| Endpoint | Kiểm tra | Mục đích |
|
||||
|----------|----------|----------|
|
||||
| `/health` | Tất cả dependencies | Trạng thái đầy đủ |
|
||||
| `/health/live` | App đang chạy | Kubernetes liveness |
|
||||
| `/health/ready` | DB + IAM Service | Kubernetes readiness |
|
||||
|
||||
## Giám Sát
|
||||
|
||||
### Metrics (Prometheus)
|
||||
|
||||
- `merchant_registrations_total` - Số merchant đăng ký
|
||||
- `merchant_approvals_total` - Số merchant được phê duyệt
|
||||
- `shop_creations_total` - Số shop được tạo
|
||||
- `pos_auth_attempts_total` - Số lần đăng nhập POS
|
||||
- `pos_auth_failures_total` - Số lần đăng nhập POS thất bại
|
||||
|
||||
### Logging
|
||||
|
||||
Structured Serilog logging với:
|
||||
- Request/Response logging
|
||||
- Domain event logging
|
||||
- POS authentication audit trail
|
||||
- Error tracking với stack traces
|
||||
|
||||
## Xử Lý Lỗi
|
||||
|
||||
### Error Codes
|
||||
|
||||
| Code | HTTP Status | Description |
|
||||
|------|-------------|-------------|
|
||||
| `MERCHANT_NOT_FOUND` | 404 | Merchant không tồn tại |
|
||||
| `MERCHANT_NOT_ACTIVE` | 400 | Merchant chưa được phê duyệt |
|
||||
| `SHOP_SLUG_EXISTS` | 409 | Slug shop đã tồn tại |
|
||||
| `STAFF_ALREADY_EXISTS` | 409 | Nhân viên đã tồn tại |
|
||||
| `INVALID_PIN` | 401 | PIN không đúng |
|
||||
| `DEVICE_NOT_REGISTERED` | 403 | Thiết bị chưa đăng ký |
|
||||
|
||||
### Response Format
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.4",
|
||||
"title": "Bad Request",
|
||||
"status": 400,
|
||||
"detail": "Only active merchants can create shops.",
|
||||
"errorCode": "MERCHANT_NOT_ACTIVE",
|
||||
"traceId": "00-1234567890abcdef-1234567890abcdef-00"
|
||||
}
|
||||
```
|
||||
303
services/merchant-service-net/docs/vi/README.md
Normal file
303
services/merchant-service-net/docs/vi/README.md
Normal file
@@ -0,0 +1,303 @@
|
||||
# Merchant Service .NET 10
|
||||
|
||||
> **Service quản lý Merchant (Shop Owner), Shop, và Merchant Staff cho hệ sinh thái GoodGo.**
|
||||
|
||||
## Tổng Quan
|
||||
|
||||
Merchant Service cung cấp các chức năng quản lý:
|
||||
|
||||
- **Merchant Management** - Đăng ký, xác minh, quản lý Merchant (Shop Owner)
|
||||
- **Shop Management** - Tạo và quản lý cửa hàng (Online, Cửa hàng vật lý, Hybrid)
|
||||
- **Staff Management** - Quản lý nhân viên, phân quyền cho POS System
|
||||
- **Branch Management** - Quản lý chi nhánh cửa hàng
|
||||
- **POS Authentication** - Xác thực PIN cho thiết bị POS
|
||||
- **Multi-category Support** - Hỗ trợ đa ngành nghề kinh doanh
|
||||
|
||||
## Yêu Cầu
|
||||
|
||||
| Yêu cầu | Phiên bản |
|
||||
|---------|-----------|
|
||||
| .NET SDK | 10.0.101+ |
|
||||
| Docker | 24.0+ |
|
||||
| PostgreSQL | 15+ |
|
||||
|
||||
## Bắt Đầu Nhanh
|
||||
|
||||
### Chạy với Docker
|
||||
|
||||
```bash
|
||||
cd deployments/local
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Chạy Local
|
||||
|
||||
```bash
|
||||
cd services/merchant-service-net
|
||||
dotnet restore
|
||||
dotnet build
|
||||
dotnet run --project src/MerchantService.API
|
||||
```
|
||||
|
||||
## Database Migrations
|
||||
|
||||
### Yêu Cầu
|
||||
|
||||
```bash
|
||||
# Cài đặt EF Core tools (một lần)
|
||||
dotnet tool install --global dotnet-ef
|
||||
```
|
||||
|
||||
### Tạo Migration
|
||||
|
||||
```bash
|
||||
# Tạo migration mới
|
||||
dotnet ef migrations add <TenMigration> \
|
||||
--project src/MerchantService.Infrastructure \
|
||||
--startup-project src/MerchantService.API
|
||||
```
|
||||
|
||||
### Áp Dụng Migration
|
||||
|
||||
```bash
|
||||
# Áp dụng migrations vào database
|
||||
dotnet ef database update \
|
||||
--project src/MerchantService.Infrastructure \
|
||||
--startup-project src/MerchantService.API
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Merchant Endpoints (`/api/v1/merchants`)
|
||||
|
||||
| Method | Endpoint | Mô Tả | Auth |
|
||||
|--------|----------|-------|------|
|
||||
| `POST` | `/api/v1/merchants/register` | Đăng ký trở thành Merchant | ✅ User |
|
||||
| `GET` | `/api/v1/merchants/me` | Lấy thông tin Merchant hiện tại | ✅ Merchant |
|
||||
| `PUT` | `/api/v1/merchants/me` | Cập nhật thông tin Merchant | ✅ Merchant |
|
||||
| `POST` | `/api/v1/merchants/me/verify` | Submit verification documents | ✅ Merchant |
|
||||
|
||||
### Shop Endpoints (`/api/v1/shops`)
|
||||
|
||||
| Method | Endpoint | Mô Tả | Auth |
|
||||
|--------|----------|-------|------|
|
||||
| `GET` | `/api/v1/shops` | Danh sách Shop của Merchant | ✅ Merchant |
|
||||
| `POST` | `/api/v1/shops` | Tạo Shop mới | ✅ Merchant |
|
||||
| `GET` | `/api/v1/shops/{id}` | Lấy Shop theo ID | ✅ Merchant |
|
||||
| `PUT` | `/api/v1/shops/{id}` | Cập nhật Shop | ✅ Merchant |
|
||||
| `DELETE` | `/api/v1/shops/{id}` | Đóng Shop | ✅ Merchant |
|
||||
| `GET` | `/api/v1/shops/slug/{slug}` | Lấy Shop theo slug (public) | ❌ |
|
||||
|
||||
### Shop Branch Endpoints
|
||||
|
||||
| Method | Endpoint | Mô Tả | Auth |
|
||||
|--------|----------|-------|------|
|
||||
| `GET` | `/api/v1/shops/{shopId}/branches` | Danh sách chi nhánh | ✅ |
|
||||
| `POST` | `/api/v1/shops/{shopId}/branches` | Tạo chi nhánh mới | ✅ Merchant |
|
||||
| `PUT` | `/api/v1/shops/{shopId}/branches/{id}` | Cập nhật chi nhánh | ✅ Merchant |
|
||||
| `DELETE` | `/api/v1/shops/{shopId}/branches/{id}` | Xóa chi nhánh | ✅ Merchant |
|
||||
| `GET` | `/api/v1/shops/nearby` | Tìm cửa hàng gần đây | ❌ |
|
||||
|
||||
### Merchant Staff Endpoints
|
||||
|
||||
| Method | Endpoint | Mô Tả | Auth |
|
||||
|--------|----------|-------|------|
|
||||
| `GET` | `/api/v1/merchants/me/staff` | Danh sách nhân viên | ✅ Merchant |
|
||||
| `POST` | `/api/v1/merchants/me/staff/invite` | Mời nhân viên | ✅ Merchant |
|
||||
| `PUT` | `/api/v1/merchants/me/staff/{id}` | Cập nhật nhân viên | ✅ Merchant |
|
||||
| `DELETE` | `/api/v1/merchants/me/staff/{id}` | Xóa nhân viên | ✅ Merchant |
|
||||
| `POST` | `/api/v1/staff/accept-invite` | Chấp nhận lời mời | ✅ User |
|
||||
|
||||
### POS Endpoints (`/api/v1/pos`)
|
||||
|
||||
| Method | Endpoint | Mô Tả | Auth |
|
||||
|--------|----------|-------|------|
|
||||
| `POST` | `/api/v1/pos/auth/pin` | Đăng nhập POS bằng PIN | ✅ Staff |
|
||||
| `POST` | `/api/v1/pos/devices/register` | Đăng ký thiết bị POS | ✅ Staff |
|
||||
| `GET` | `/api/v1/pos/me` | Thông tin Staff hiện tại | ✅ Staff |
|
||||
|
||||
### Admin Endpoints
|
||||
|
||||
| Method | Endpoint | Mô Tả | Auth |
|
||||
|--------|----------|-------|------|
|
||||
| `GET` | `/api/v1/admin/merchants` | Danh sách tất cả Merchants | ✅ Admin |
|
||||
| `POST` | `/api/v1/admin/merchants/{id}/approve` | Phê duyệt Merchant | ✅ Admin |
|
||||
| `POST` | `/api/v1/admin/merchants/{id}/suspend` | Tạm ngưng Merchant | ✅ Admin |
|
||||
|
||||
### Health Checks
|
||||
|
||||
| Endpoint | Mục Đích |
|
||||
|----------|----------|
|
||||
| `/health` | Trạng thái health đầy đủ |
|
||||
| `/health/live` | Kiểm tra sống |
|
||||
| `/health/ready` | Kiểm tra sẵn sàng |
|
||||
|
||||
## Quy Trình Đăng Ký Merchant
|
||||
|
||||
### Bước 1: Đăng Ký Trở Thành Merchant
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:5003/api/v1/merchants/register \
|
||||
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"businessName": "Cà Phê Sài Gòn",
|
||||
"type": "Company",
|
||||
"taxId": "0123456789",
|
||||
"businessLicenseNumber": "BL-2024-001"
|
||||
}'
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"merchantId": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"businessName": "Cà Phê Sài Gòn",
|
||||
"status": "PendingApproval"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Bước 2: Chờ Admin Phê Duyệt
|
||||
|
||||
Admin sẽ review và phê duyệt Merchant. Sau khi được phê duyệt:
|
||||
- Role `Merchant` sẽ được gán cho user trong IAM Service
|
||||
- Status chuyển thành `Active`
|
||||
|
||||
### Bước 3: Tạo Shop
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:5003/api/v1/shops \
|
||||
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Cà Phê Sài Gòn - Quận 1",
|
||||
"slug": "ca-phe-sai-gon-q1",
|
||||
"type": "Hybrid",
|
||||
"category": "FoodBeverage",
|
||||
"phone": "0901234567",
|
||||
"email": "q1@caphesaigon.vn"
|
||||
}'
|
||||
```
|
||||
|
||||
### Bước 4: Thêm Chi Nhánh (Cho cửa hàng vật lý)
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:5003/api/v1/shops/{shopId}/branches \
|
||||
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Chi Nhánh Lê Lợi",
|
||||
"street": "123 Lê Lợi",
|
||||
"district": "Quận 1",
|
||||
"city": "TP. Hồ Chí Minh",
|
||||
"latitude": 10.7769,
|
||||
"longitude": 106.7009
|
||||
}'
|
||||
```
|
||||
|
||||
### Bước 5: Mời Nhân Viên
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:5003/api/v1/merchants/me/staff/invite \
|
||||
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "nhanvien@example.com",
|
||||
"role": "Cashier",
|
||||
"shopId": "shop-uuid",
|
||||
"branchId": "branch-uuid"
|
||||
}'
|
||||
```
|
||||
|
||||
## Cấu Hình
|
||||
|
||||
### Biến Môi Trường
|
||||
|
||||
| Biến | Mô Tả | Mặc định |
|
||||
|------|-------|----------|
|
||||
| `ASPNETCORE_ENVIRONMENT` | Môi trường | `Development` |
|
||||
| `DATABASE_URL` | PostgreSQL connection | - |
|
||||
| `Jwt__Authority` | IAM Service URL | `http://iam-service-net:8080` |
|
||||
| `IamService__BaseUrl` | IAM Service base URL | - |
|
||||
| `WalletService__BaseUrl` | Wallet Service base URL | - |
|
||||
|
||||
### appsettings.json
|
||||
|
||||
```json
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Host=localhost;Port=5432;Database=merchant;Username=postgres;Password=postgres"
|
||||
},
|
||||
"Jwt": {
|
||||
"Authority": "http://localhost:5001"
|
||||
},
|
||||
"IamService": {
|
||||
"BaseUrl": "http://localhost:5001"
|
||||
},
|
||||
"WalletService": {
|
||||
"BaseUrl": "http://localhost:5004"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Integration với Các Services Khác
|
||||
|
||||
### IAM Service
|
||||
- Validate User token
|
||||
- Assign Merchant/MerchantStaff roles
|
||||
- Get Organization hierarchy
|
||||
|
||||
### Wallet Service
|
||||
- Merchant settlement
|
||||
- Commission calculation
|
||||
- Balance check
|
||||
|
||||
### Membership Service
|
||||
- Customer visit tracking → Add EXP
|
||||
- Member level → Special discount at shop
|
||||
|
||||
### Chat Service
|
||||
- Shop support channel
|
||||
- Staff-Customer conversation
|
||||
|
||||
### Gift Voucher Service (Planned)
|
||||
- Create voucher campaigns
|
||||
- Voucher redemption at shop
|
||||
|
||||
## Kiểm Thử
|
||||
|
||||
```bash
|
||||
# Unit tests
|
||||
dotnet test tests/MerchantService.UnitTests
|
||||
|
||||
# Functional tests
|
||||
dotnet test tests/MerchantService.FunctionalTests
|
||||
```
|
||||
|
||||
## Triển Khai
|
||||
|
||||
### Docker Build
|
||||
|
||||
```bash
|
||||
docker build -t goodgo/merchant-service:latest .
|
||||
docker run -p 5003:8080 --env-file .env goodgo/merchant-service:latest
|
||||
```
|
||||
|
||||
### Kubernetes
|
||||
|
||||
Xem file `deployments/kubernetes/merchant-service.yaml`
|
||||
|
||||
## Swagger UI
|
||||
|
||||
Sau khi chạy service, truy cập Swagger UI tại:
|
||||
- **Local**: http://localhost:5003/swagger
|
||||
- **Docker**: http://localhost/api/v1/merchants/swagger
|
||||
|
||||
## Tài Nguyên
|
||||
|
||||
- [Clean Architecture Documentation](../../docs/vi/architecture/)
|
||||
- [API Design Skill](../../.agent/skills/api-design/SKILL.md)
|
||||
- [CQRS Pattern Skill](../../.agent/skills/cqrs-mediatr/SKILL.md)
|
||||
Reference in New Issue
Block a user