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:
Ho Ngoc Hai
2026-01-15 18:03:00 +07:00
parent 8e53fadb19
commit c921098788
7 changed files with 2611 additions and 42 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -0,0 +1,106 @@
# Merchant Service .NET 10
> **Merchant & Shop Management Service for GoodGo Platform**
[![.NET](https://img.shields.io/badge/.NET-10.0-512BD4)](https://dotnet.microsoft.com/)
[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-15+-4169E1)](https://www.postgresql.org/)
[![Docker](https://img.shields.io/badge/Docker-Ready-2496ED)](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.

View 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"
}
```

View 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)

View 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"
}
```

View 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)